pane.rs

   1use crate::{
   2    CloseWindow, NewFile, NewTerminal, OpenInTerminal, OpenOptions, OpenTerminal, OpenVisible,
   3    SplitDirection, ToggleFileFinder, ToggleProjectSymbols, ToggleZoom, Workspace,
   4    WorkspaceItemBuilder,
   5    item::{
   6        ActivateOnClose, ClosePosition, Item, ItemHandle, ItemSettings, PreviewTabsSettings,
   7        ProjectItemKind, ShowCloseButton, ShowDiagnostics, TabContentParams, TabTooltipContent,
   8        WeakItemHandle,
   9    },
  10    move_item,
  11    notifications::NotifyResultExt,
  12    toolbar::Toolbar,
  13    workspace_settings::{AutosaveSetting, TabBarSettings, WorkspaceSettings},
  14};
  15use anyhow::Result;
  16use collections::{BTreeSet, HashMap, HashSet, VecDeque};
  17use futures::{StreamExt, stream::FuturesUnordered};
  18use gpui::{
  19    Action, AnyElement, App, AsyncWindowContext, ClickEvent, ClipboardItem, Context, Corner, Div,
  20    DragMoveEvent, Entity, EntityId, EventEmitter, ExternalPaths, FocusHandle, FocusOutEvent,
  21    Focusable, KeyContext, MouseButton, MouseDownEvent, NavigationDirection, Pixels, Point,
  22    PromptLevel, Render, ScrollHandle, Subscription, Task, WeakEntity, WeakFocusHandle, Window,
  23    actions, anchored, deferred, impl_actions, prelude::*,
  24};
  25use itertools::Itertools;
  26use language::DiagnosticSeverity;
  27use parking_lot::Mutex;
  28use project::{Project, ProjectEntryId, ProjectPath, WorktreeId};
  29use schemars::JsonSchema;
  30use serde::Deserialize;
  31use settings::{Settings, SettingsStore};
  32use std::{
  33    any::Any,
  34    cmp, fmt, mem,
  35    ops::ControlFlow,
  36    path::PathBuf,
  37    rc::Rc,
  38    sync::{
  39        Arc,
  40        atomic::{AtomicUsize, Ordering},
  41    },
  42};
  43use theme::ThemeSettings;
  44use ui::{
  45    ButtonSize, Color, ContextMenu, ContextMenuEntry, ContextMenuItem, DecoratedIcon, IconButton,
  46    IconButtonShape, IconDecoration, IconDecorationKind, IconName, IconSize, Indicator, Label,
  47    PopoverMenu, PopoverMenuHandle, ScrollableHandle, Tab, TabBar, TabPosition, Tooltip,
  48    prelude::*, right_click_menu,
  49};
  50use util::{ResultExt, debug_panic, maybe, truncate_and_remove_front};
  51
  52/// A selected entry in e.g. project panel.
  53#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
  54pub struct SelectedEntry {
  55    pub worktree_id: WorktreeId,
  56    pub entry_id: ProjectEntryId,
  57}
  58
  59/// A group of selected entries from project panel.
  60#[derive(Debug)]
  61pub struct DraggedSelection {
  62    pub active_selection: SelectedEntry,
  63    pub marked_selections: Arc<BTreeSet<SelectedEntry>>,
  64}
  65
  66impl DraggedSelection {
  67    pub fn items<'a>(&'a self) -> Box<dyn Iterator<Item = &'a SelectedEntry> + 'a> {
  68        if self.marked_selections.contains(&self.active_selection) {
  69            Box::new(self.marked_selections.iter())
  70        } else {
  71            Box::new(std::iter::once(&self.active_selection))
  72        }
  73    }
  74}
  75
  76#[derive(Clone, Copy, PartialEq, Debug, Deserialize, JsonSchema)]
  77#[serde(rename_all = "snake_case")]
  78pub enum SaveIntent {
  79    /// write all files (even if unchanged)
  80    /// prompt before overwriting on-disk changes
  81    Save,
  82    /// same as Save, but without auto formatting
  83    SaveWithoutFormat,
  84    /// write any files that have local changes
  85    /// prompt before overwriting on-disk changes
  86    SaveAll,
  87    /// always prompt for a new path
  88    SaveAs,
  89    /// prompt "you have unsaved changes" before writing
  90    Close,
  91    /// write all dirty files, don't prompt on conflict
  92    Overwrite,
  93    /// skip all save-related behavior
  94    Skip,
  95}
  96
  97#[derive(Clone, PartialEq, Debug, Deserialize, JsonSchema, Default)]
  98pub struct ActivateItem(pub usize);
  99
 100#[derive(Clone, PartialEq, Debug, Deserialize, JsonSchema, Default)]
 101#[serde(deny_unknown_fields)]
 102pub struct CloseActiveItem {
 103    pub save_intent: Option<SaveIntent>,
 104    #[serde(default)]
 105    pub close_pinned: bool,
 106}
 107
 108#[derive(Clone, PartialEq, Debug, Deserialize, JsonSchema, Default)]
 109#[serde(deny_unknown_fields)]
 110pub struct CloseInactiveItems {
 111    pub save_intent: Option<SaveIntent>,
 112    #[serde(default)]
 113    pub close_pinned: bool,
 114}
 115
 116#[derive(Clone, PartialEq, Debug, Deserialize, JsonSchema, Default)]
 117#[serde(deny_unknown_fields)]
 118pub struct CloseAllItems {
 119    pub save_intent: Option<SaveIntent>,
 120    #[serde(default)]
 121    pub close_pinned: bool,
 122}
 123
 124#[derive(Clone, PartialEq, Debug, Deserialize, JsonSchema, Default)]
 125#[serde(deny_unknown_fields)]
 126pub struct CloseCleanItems {
 127    #[serde(default)]
 128    pub close_pinned: bool,
 129}
 130
 131#[derive(Clone, PartialEq, Debug, Deserialize, JsonSchema, Default)]
 132#[serde(deny_unknown_fields)]
 133pub struct CloseItemsToTheRight {
 134    #[serde(default)]
 135    pub close_pinned: bool,
 136}
 137
 138#[derive(Clone, PartialEq, Debug, Deserialize, JsonSchema, Default)]
 139#[serde(deny_unknown_fields)]
 140pub struct CloseItemsToTheLeft {
 141    #[serde(default)]
 142    pub close_pinned: bool,
 143}
 144
 145#[derive(Clone, PartialEq, Debug, Deserialize, JsonSchema, Default)]
 146#[serde(deny_unknown_fields)]
 147pub struct RevealInProjectPanel {
 148    #[serde(skip)]
 149    pub entry_id: Option<u64>,
 150}
 151
 152#[derive(Clone, PartialEq, Debug, Deserialize, JsonSchema, Default)]
 153#[serde(deny_unknown_fields)]
 154pub struct DeploySearch {
 155    #[serde(default)]
 156    pub replace_enabled: bool,
 157}
 158
 159impl_actions!(
 160    pane,
 161    [
 162        CloseAllItems,
 163        CloseActiveItem,
 164        CloseCleanItems,
 165        CloseItemsToTheLeft,
 166        CloseItemsToTheRight,
 167        CloseInactiveItems,
 168        ActivateItem,
 169        RevealInProjectPanel,
 170        DeploySearch,
 171    ]
 172);
 173
 174actions!(
 175    pane,
 176    [
 177        ActivatePreviousItem,
 178        ActivateNextItem,
 179        ActivateLastItem,
 180        AlternateFile,
 181        GoBack,
 182        GoForward,
 183        JoinIntoNext,
 184        JoinAll,
 185        ReopenClosedItem,
 186        SplitLeft,
 187        SplitUp,
 188        SplitRight,
 189        SplitDown,
 190        SplitHorizontal,
 191        SplitVertical,
 192        SwapItemLeft,
 193        SwapItemRight,
 194        TogglePreviewTab,
 195        TogglePinTab,
 196    ]
 197);
 198
 199impl DeploySearch {
 200    pub fn find() -> Self {
 201        Self {
 202            replace_enabled: false,
 203        }
 204    }
 205}
 206
 207const MAX_NAVIGATION_HISTORY_LEN: usize = 1024;
 208
 209pub enum Event {
 210    AddItem {
 211        item: Box<dyn ItemHandle>,
 212    },
 213    ActivateItem {
 214        local: bool,
 215        focus_changed: bool,
 216    },
 217    Remove {
 218        focus_on_pane: Option<Entity<Pane>>,
 219    },
 220    RemoveItem {
 221        idx: usize,
 222    },
 223    RemovedItem {
 224        item: Box<dyn ItemHandle>,
 225    },
 226    Split(SplitDirection),
 227    JoinAll,
 228    JoinIntoNext,
 229    ChangeItemTitle,
 230    Focus,
 231    ZoomIn,
 232    ZoomOut,
 233    UserSavedItem {
 234        item: Box<dyn WeakItemHandle>,
 235        save_intent: SaveIntent,
 236    },
 237}
 238
 239impl fmt::Debug for Event {
 240    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
 241        match self {
 242            Event::AddItem { item } => f
 243                .debug_struct("AddItem")
 244                .field("item", &item.item_id())
 245                .finish(),
 246            Event::ActivateItem { local, .. } => f
 247                .debug_struct("ActivateItem")
 248                .field("local", local)
 249                .finish(),
 250            Event::Remove { .. } => f.write_str("Remove"),
 251            Event::RemoveItem { idx } => f.debug_struct("RemoveItem").field("idx", idx).finish(),
 252            Event::RemovedItem { item } => f
 253                .debug_struct("RemovedItem")
 254                .field("item", &item.item_id())
 255                .finish(),
 256            Event::Split(direction) => f
 257                .debug_struct("Split")
 258                .field("direction", direction)
 259                .finish(),
 260            Event::JoinAll => f.write_str("JoinAll"),
 261            Event::JoinIntoNext => f.write_str("JoinIntoNext"),
 262            Event::ChangeItemTitle => f.write_str("ChangeItemTitle"),
 263            Event::Focus => f.write_str("Focus"),
 264            Event::ZoomIn => f.write_str("ZoomIn"),
 265            Event::ZoomOut => f.write_str("ZoomOut"),
 266            Event::UserSavedItem { item, save_intent } => f
 267                .debug_struct("UserSavedItem")
 268                .field("item", &item.id())
 269                .field("save_intent", save_intent)
 270                .finish(),
 271        }
 272    }
 273}
 274
 275/// A container for 0 to many items that are open in the workspace.
 276/// Treats all items uniformly via the [`ItemHandle`] trait, whether it's an editor, search results multibuffer, terminal or something else,
 277/// responsible for managing item tabs, focus and zoom states and drag and drop features.
 278/// Can be split, see `PaneGroup` for more details.
 279pub struct Pane {
 280    alternate_file_items: (
 281        Option<Box<dyn WeakItemHandle>>,
 282        Option<Box<dyn WeakItemHandle>>,
 283    ),
 284    focus_handle: FocusHandle,
 285    items: Vec<Box<dyn ItemHandle>>,
 286    activation_history: Vec<ActivationHistoryEntry>,
 287    next_activation_timestamp: Arc<AtomicUsize>,
 288    zoomed: bool,
 289    was_focused: bool,
 290    active_item_index: usize,
 291    preview_item_id: Option<EntityId>,
 292    last_focus_handle_by_item: HashMap<EntityId, WeakFocusHandle>,
 293    nav_history: NavHistory,
 294    toolbar: Entity<Toolbar>,
 295    pub(crate) workspace: WeakEntity<Workspace>,
 296    project: WeakEntity<Project>,
 297    pub drag_split_direction: Option<SplitDirection>,
 298    can_drop_predicate: Option<Arc<dyn Fn(&dyn Any, &mut Window, &mut App) -> bool>>,
 299    custom_drop_handle: Option<
 300        Arc<dyn Fn(&mut Pane, &dyn Any, &mut Window, &mut Context<Pane>) -> ControlFlow<(), ()>>,
 301    >,
 302    can_split_predicate:
 303        Option<Arc<dyn Fn(&mut Self, &dyn Any, &mut Window, &mut Context<Self>) -> bool>>,
 304    should_display_tab_bar: Rc<dyn Fn(&Window, &mut Context<Pane>) -> bool>,
 305    render_tab_bar_buttons: Rc<
 306        dyn Fn(
 307            &mut Pane,
 308            &mut Window,
 309            &mut Context<Pane>,
 310        ) -> (Option<AnyElement>, Option<AnyElement>),
 311    >,
 312    render_tab_bar: Rc<dyn Fn(&mut Pane, &mut Window, &mut Context<Pane>) -> AnyElement>,
 313    show_tab_bar_buttons: bool,
 314    _subscriptions: Vec<Subscription>,
 315    tab_bar_scroll_handle: ScrollHandle,
 316    /// Is None if navigation buttons are permanently turned off (and should not react to setting changes).
 317    /// Otherwise, when `display_nav_history_buttons` is Some, it determines whether nav buttons should be displayed.
 318    display_nav_history_buttons: Option<bool>,
 319    double_click_dispatch_action: Box<dyn Action>,
 320    save_modals_spawned: HashSet<EntityId>,
 321    close_pane_if_empty: bool,
 322    pub new_item_context_menu_handle: PopoverMenuHandle<ContextMenu>,
 323    pub split_item_context_menu_handle: PopoverMenuHandle<ContextMenu>,
 324    pinned_tab_count: usize,
 325    diagnostics: HashMap<ProjectPath, DiagnosticSeverity>,
 326    zoom_out_on_close: bool,
 327    /// If a certain project item wants to get recreated with specific data, it can persist its data before the recreation here.
 328    pub project_item_restoration_data: HashMap<ProjectItemKind, Box<dyn Any + Send>>,
 329}
 330
 331pub struct ActivationHistoryEntry {
 332    pub entity_id: EntityId,
 333    pub timestamp: usize,
 334}
 335
 336pub struct ItemNavHistory {
 337    history: NavHistory,
 338    item: Arc<dyn WeakItemHandle>,
 339    is_preview: bool,
 340}
 341
 342#[derive(Clone)]
 343pub struct NavHistory(Arc<Mutex<NavHistoryState>>);
 344
 345struct NavHistoryState {
 346    mode: NavigationMode,
 347    backward_stack: VecDeque<NavigationEntry>,
 348    forward_stack: VecDeque<NavigationEntry>,
 349    closed_stack: VecDeque<NavigationEntry>,
 350    paths_by_item: HashMap<EntityId, (ProjectPath, Option<PathBuf>)>,
 351    pane: WeakEntity<Pane>,
 352    next_timestamp: Arc<AtomicUsize>,
 353}
 354
 355#[derive(Debug, Copy, Clone)]
 356pub enum NavigationMode {
 357    Normal,
 358    GoingBack,
 359    GoingForward,
 360    ClosingItem,
 361    ReopeningClosedItem,
 362    Disabled,
 363}
 364
 365impl Default for NavigationMode {
 366    fn default() -> Self {
 367        Self::Normal
 368    }
 369}
 370
 371pub struct NavigationEntry {
 372    pub item: Arc<dyn WeakItemHandle>,
 373    pub data: Option<Box<dyn Any + Send>>,
 374    pub timestamp: usize,
 375    pub is_preview: bool,
 376}
 377
 378#[derive(Clone)]
 379pub struct DraggedTab {
 380    pub pane: Entity<Pane>,
 381    pub item: Box<dyn ItemHandle>,
 382    pub ix: usize,
 383    pub detail: usize,
 384    pub is_active: bool,
 385}
 386
 387impl EventEmitter<Event> for Pane {}
 388
 389impl Pane {
 390    pub fn new(
 391        workspace: WeakEntity<Workspace>,
 392        project: Entity<Project>,
 393        next_timestamp: Arc<AtomicUsize>,
 394        can_drop_predicate: Option<Arc<dyn Fn(&dyn Any, &mut Window, &mut App) -> bool + 'static>>,
 395        double_click_dispatch_action: Box<dyn Action>,
 396        window: &mut Window,
 397        cx: &mut Context<Self>,
 398    ) -> Self {
 399        let focus_handle = cx.focus_handle();
 400
 401        let subscriptions = vec![
 402            cx.on_focus(&focus_handle, window, Pane::focus_in),
 403            cx.on_focus_in(&focus_handle, window, Pane::focus_in),
 404            cx.on_focus_out(&focus_handle, window, Pane::focus_out),
 405            cx.observe_global::<SettingsStore>(Self::settings_changed),
 406            cx.subscribe(&project, Self::project_events),
 407        ];
 408
 409        let handle = cx.entity().downgrade();
 410        Self {
 411            alternate_file_items: (None, None),
 412            focus_handle,
 413            items: Vec::new(),
 414            activation_history: Vec::new(),
 415            next_activation_timestamp: next_timestamp.clone(),
 416            was_focused: false,
 417            zoomed: false,
 418            active_item_index: 0,
 419            preview_item_id: None,
 420            last_focus_handle_by_item: Default::default(),
 421            nav_history: NavHistory(Arc::new(Mutex::new(NavHistoryState {
 422                mode: NavigationMode::Normal,
 423                backward_stack: Default::default(),
 424                forward_stack: Default::default(),
 425                closed_stack: Default::default(),
 426                paths_by_item: Default::default(),
 427                pane: handle.clone(),
 428                next_timestamp,
 429            }))),
 430            toolbar: cx.new(|_| Toolbar::new()),
 431            tab_bar_scroll_handle: ScrollHandle::new(),
 432            drag_split_direction: None,
 433            workspace,
 434            project: project.downgrade(),
 435            can_drop_predicate,
 436            custom_drop_handle: None,
 437            can_split_predicate: None,
 438            should_display_tab_bar: Rc::new(|_, cx| TabBarSettings::get_global(cx).show),
 439            render_tab_bar_buttons: Rc::new(default_render_tab_bar_buttons),
 440            render_tab_bar: Rc::new(Self::render_tab_bar),
 441            show_tab_bar_buttons: TabBarSettings::get_global(cx).show_tab_bar_buttons,
 442            display_nav_history_buttons: Some(
 443                TabBarSettings::get_global(cx).show_nav_history_buttons,
 444            ),
 445            _subscriptions: subscriptions,
 446            double_click_dispatch_action,
 447            save_modals_spawned: HashSet::default(),
 448            close_pane_if_empty: true,
 449            split_item_context_menu_handle: Default::default(),
 450            new_item_context_menu_handle: Default::default(),
 451            pinned_tab_count: 0,
 452            diagnostics: Default::default(),
 453            zoom_out_on_close: true,
 454            project_item_restoration_data: HashMap::default(),
 455        }
 456    }
 457
 458    fn alternate_file(&mut self, window: &mut Window, cx: &mut Context<Pane>) {
 459        let (_, alternative) = &self.alternate_file_items;
 460        if let Some(alternative) = alternative {
 461            let existing = self
 462                .items()
 463                .find_position(|item| item.item_id() == alternative.id());
 464            if let Some((ix, _)) = existing {
 465                self.activate_item(ix, true, true, window, cx);
 466            } else if let Some(upgraded) = alternative.upgrade() {
 467                self.add_item(upgraded, true, true, None, window, cx);
 468            }
 469        }
 470    }
 471
 472    pub fn track_alternate_file_items(&mut self) {
 473        if let Some(item) = self.active_item().map(|item| item.downgrade_item()) {
 474            let (current, _) = &self.alternate_file_items;
 475            match current {
 476                Some(current) => {
 477                    if current.id() != item.id() {
 478                        self.alternate_file_items =
 479                            (Some(item), self.alternate_file_items.0.take());
 480                    }
 481                }
 482                None => {
 483                    self.alternate_file_items = (Some(item), None);
 484                }
 485            }
 486        }
 487    }
 488
 489    pub fn has_focus(&self, window: &Window, cx: &App) -> bool {
 490        // We not only check whether our focus handle contains focus, but also
 491        // whether the active item might have focus, because we might have just activated an item
 492        // that hasn't rendered yet.
 493        // Before the next render, we might transfer focus
 494        // to the item, and `focus_handle.contains_focus` returns false because the `active_item`
 495        // is not hooked up to us in the dispatch tree.
 496        self.focus_handle.contains_focused(window, cx)
 497            || self.active_item().map_or(false, |item| {
 498                item.item_focus_handle(cx).contains_focused(window, cx)
 499            })
 500    }
 501
 502    fn focus_in(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 503        if !self.was_focused {
 504            self.was_focused = true;
 505            cx.emit(Event::Focus);
 506            cx.notify();
 507        }
 508
 509        self.toolbar.update(cx, |toolbar, cx| {
 510            toolbar.focus_changed(true, window, cx);
 511        });
 512
 513        if let Some(active_item) = self.active_item() {
 514            if self.focus_handle.is_focused(window) {
 515                // Schedule a redraw next frame, so that the focus changes below take effect
 516                cx.on_next_frame(window, |_, _, cx| {
 517                    cx.notify();
 518                });
 519
 520                // Pane was focused directly. We need to either focus a view inside the active item,
 521                // or focus the active item itself
 522                if let Some(weak_last_focus_handle) =
 523                    self.last_focus_handle_by_item.get(&active_item.item_id())
 524                {
 525                    if let Some(focus_handle) = weak_last_focus_handle.upgrade() {
 526                        focus_handle.focus(window);
 527                        return;
 528                    }
 529                }
 530
 531                active_item.item_focus_handle(cx).focus(window);
 532            } else if let Some(focused) = window.focused(cx) {
 533                if !self.context_menu_focused(window, cx) {
 534                    self.last_focus_handle_by_item
 535                        .insert(active_item.item_id(), focused.downgrade());
 536                }
 537            }
 538        }
 539    }
 540
 541    pub fn context_menu_focused(&self, window: &mut Window, cx: &mut Context<Self>) -> bool {
 542        self.new_item_context_menu_handle.is_focused(window, cx)
 543            || self.split_item_context_menu_handle.is_focused(window, cx)
 544    }
 545
 546    fn focus_out(&mut self, _event: FocusOutEvent, window: &mut Window, cx: &mut Context<Self>) {
 547        self.was_focused = false;
 548        self.toolbar.update(cx, |toolbar, cx| {
 549            toolbar.focus_changed(false, window, cx);
 550        });
 551        cx.notify();
 552    }
 553
 554    fn project_events(
 555        &mut self,
 556        _project: Entity<Project>,
 557        event: &project::Event,
 558        cx: &mut Context<Self>,
 559    ) {
 560        match event {
 561            project::Event::DiskBasedDiagnosticsFinished { .. }
 562            | project::Event::DiagnosticsUpdated { .. } => {
 563                if ItemSettings::get_global(cx).show_diagnostics != ShowDiagnostics::Off {
 564                    self.update_diagnostics(cx);
 565                    cx.notify();
 566                }
 567            }
 568            _ => {}
 569        }
 570    }
 571
 572    fn update_diagnostics(&mut self, cx: &mut Context<Self>) {
 573        let Some(project) = self.project.upgrade() else {
 574            return;
 575        };
 576        let show_diagnostics = ItemSettings::get_global(cx).show_diagnostics;
 577        self.diagnostics = if show_diagnostics != ShowDiagnostics::Off {
 578            project
 579                .read(cx)
 580                .diagnostic_summaries(false, cx)
 581                .filter_map(|(project_path, _, diagnostic_summary)| {
 582                    if diagnostic_summary.error_count > 0 {
 583                        Some((project_path, DiagnosticSeverity::ERROR))
 584                    } else if diagnostic_summary.warning_count > 0
 585                        && show_diagnostics != ShowDiagnostics::Errors
 586                    {
 587                        Some((project_path, DiagnosticSeverity::WARNING))
 588                    } else {
 589                        None
 590                    }
 591                })
 592                .collect()
 593        } else {
 594            HashMap::default()
 595        }
 596    }
 597
 598    fn settings_changed(&mut self, cx: &mut Context<Self>) {
 599        let tab_bar_settings = TabBarSettings::get_global(cx);
 600
 601        if let Some(display_nav_history_buttons) = self.display_nav_history_buttons.as_mut() {
 602            *display_nav_history_buttons = tab_bar_settings.show_nav_history_buttons;
 603        }
 604        self.show_tab_bar_buttons = tab_bar_settings.show_tab_bar_buttons;
 605
 606        if !PreviewTabsSettings::get_global(cx).enabled {
 607            self.preview_item_id = None;
 608        }
 609        self.update_diagnostics(cx);
 610        cx.notify();
 611    }
 612
 613    pub fn active_item_index(&self) -> usize {
 614        self.active_item_index
 615    }
 616
 617    pub fn activation_history(&self) -> &[ActivationHistoryEntry] {
 618        &self.activation_history
 619    }
 620
 621    pub fn set_should_display_tab_bar<F>(&mut self, should_display_tab_bar: F)
 622    where
 623        F: 'static + Fn(&Window, &mut Context<Pane>) -> bool,
 624    {
 625        self.should_display_tab_bar = Rc::new(should_display_tab_bar);
 626    }
 627
 628    pub fn set_can_split(
 629        &mut self,
 630        can_split_predicate: Option<
 631            Arc<dyn Fn(&mut Self, &dyn Any, &mut Window, &mut Context<Self>) -> bool + 'static>,
 632        >,
 633    ) {
 634        self.can_split_predicate = can_split_predicate;
 635    }
 636
 637    pub fn set_close_pane_if_empty(&mut self, close_pane_if_empty: bool, cx: &mut Context<Self>) {
 638        self.close_pane_if_empty = close_pane_if_empty;
 639        cx.notify();
 640    }
 641
 642    pub fn set_can_navigate(&mut self, can_navigate: bool, cx: &mut Context<Self>) {
 643        self.toolbar.update(cx, |toolbar, cx| {
 644            toolbar.set_can_navigate(can_navigate, cx);
 645        });
 646        cx.notify();
 647    }
 648
 649    pub fn set_render_tab_bar<F>(&mut self, cx: &mut Context<Self>, render: F)
 650    where
 651        F: 'static + Fn(&mut Pane, &mut Window, &mut Context<Pane>) -> AnyElement,
 652    {
 653        self.render_tab_bar = Rc::new(render);
 654        cx.notify();
 655    }
 656
 657    pub fn set_render_tab_bar_buttons<F>(&mut self, cx: &mut Context<Self>, render: F)
 658    where
 659        F: 'static
 660            + Fn(
 661                &mut Pane,
 662                &mut Window,
 663                &mut Context<Pane>,
 664            ) -> (Option<AnyElement>, Option<AnyElement>),
 665    {
 666        self.render_tab_bar_buttons = Rc::new(render);
 667        cx.notify();
 668    }
 669
 670    pub fn set_custom_drop_handle<F>(&mut self, cx: &mut Context<Self>, handle: F)
 671    where
 672        F: 'static
 673            + Fn(&mut Pane, &dyn Any, &mut Window, &mut Context<Pane>) -> ControlFlow<(), ()>,
 674    {
 675        self.custom_drop_handle = Some(Arc::new(handle));
 676        cx.notify();
 677    }
 678
 679    pub fn nav_history_for_item<T: Item>(&self, item: &Entity<T>) -> ItemNavHistory {
 680        ItemNavHistory {
 681            history: self.nav_history.clone(),
 682            item: Arc::new(item.downgrade()),
 683            is_preview: self.preview_item_id == Some(item.item_id()),
 684        }
 685    }
 686
 687    pub fn nav_history(&self) -> &NavHistory {
 688        &self.nav_history
 689    }
 690
 691    pub fn nav_history_mut(&mut self) -> &mut NavHistory {
 692        &mut self.nav_history
 693    }
 694
 695    pub fn disable_history(&mut self) {
 696        self.nav_history.disable();
 697    }
 698
 699    pub fn enable_history(&mut self) {
 700        self.nav_history.enable();
 701    }
 702
 703    pub fn can_navigate_backward(&self) -> bool {
 704        !self.nav_history.0.lock().backward_stack.is_empty()
 705    }
 706
 707    pub fn can_navigate_forward(&self) -> bool {
 708        !self.nav_history.0.lock().forward_stack.is_empty()
 709    }
 710
 711    fn navigate_backward(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 712        if let Some(workspace) = self.workspace.upgrade() {
 713            let pane = cx.entity().downgrade();
 714            window.defer(cx, move |window, cx| {
 715                workspace.update(cx, |workspace, cx| {
 716                    workspace.go_back(pane, window, cx).detach_and_log_err(cx)
 717                })
 718            })
 719        }
 720    }
 721
 722    fn navigate_forward(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 723        if let Some(workspace) = self.workspace.upgrade() {
 724            let pane = cx.entity().downgrade();
 725            window.defer(cx, move |window, cx| {
 726                workspace.update(cx, |workspace, cx| {
 727                    workspace
 728                        .go_forward(pane, window, cx)
 729                        .detach_and_log_err(cx)
 730                })
 731            })
 732        }
 733    }
 734
 735    fn history_updated(&mut self, cx: &mut Context<Self>) {
 736        self.toolbar.update(cx, |_, cx| cx.notify());
 737    }
 738
 739    pub fn preview_item_id(&self) -> Option<EntityId> {
 740        self.preview_item_id
 741    }
 742
 743    pub fn preview_item(&self) -> Option<Box<dyn ItemHandle>> {
 744        self.preview_item_id
 745            .and_then(|id| self.items.iter().find(|item| item.item_id() == id))
 746            .cloned()
 747    }
 748
 749    pub fn preview_item_idx(&self) -> Option<usize> {
 750        if let Some(preview_item_id) = self.preview_item_id {
 751            self.items
 752                .iter()
 753                .position(|item| item.item_id() == preview_item_id)
 754        } else {
 755            None
 756        }
 757    }
 758
 759    pub fn is_active_preview_item(&self, item_id: EntityId) -> bool {
 760        self.preview_item_id == Some(item_id)
 761    }
 762
 763    /// Marks the item with the given ID as the preview item.
 764    /// This will be ignored if the global setting `preview_tabs` is disabled.
 765    pub fn set_preview_item_id(&mut self, item_id: Option<EntityId>, cx: &App) {
 766        if PreviewTabsSettings::get_global(cx).enabled {
 767            self.preview_item_id = item_id;
 768        }
 769    }
 770
 771    pub(crate) fn set_pinned_count(&mut self, count: usize) {
 772        self.pinned_tab_count = count;
 773    }
 774
 775    pub(crate) fn pinned_count(&self) -> usize {
 776        self.pinned_tab_count
 777    }
 778
 779    pub fn handle_item_edit(&mut self, item_id: EntityId, cx: &App) {
 780        if let Some(preview_item) = self.preview_item() {
 781            if preview_item.item_id() == item_id && !preview_item.preserve_preview(cx) {
 782                self.set_preview_item_id(None, cx);
 783            }
 784        }
 785    }
 786
 787    pub(crate) fn open_item(
 788        &mut self,
 789        project_entry_id: Option<ProjectEntryId>,
 790        focus_item: bool,
 791        allow_preview: bool,
 792        activate: bool,
 793        suggested_position: Option<usize>,
 794        window: &mut Window,
 795        cx: &mut Context<Self>,
 796        build_item: WorkspaceItemBuilder,
 797    ) -> Box<dyn ItemHandle> {
 798        let mut existing_item = None;
 799        if let Some(project_entry_id) = project_entry_id {
 800            for (index, item) in self.items.iter().enumerate() {
 801                if item.is_singleton(cx)
 802                    && item.project_entry_ids(cx).as_slice() == [project_entry_id]
 803                {
 804                    let item = item.boxed_clone();
 805                    existing_item = Some((index, item));
 806                    break;
 807                }
 808            }
 809        }
 810        if let Some((index, existing_item)) = existing_item {
 811            // If the item is already open, and the item is a preview item
 812            // and we are not allowing items to open as preview, mark the item as persistent.
 813            if let Some(preview_item_id) = self.preview_item_id {
 814                if let Some(tab) = self.items.get(index) {
 815                    if tab.item_id() == preview_item_id && !allow_preview {
 816                        self.set_preview_item_id(None, cx);
 817                    }
 818                }
 819            }
 820            if activate {
 821                self.activate_item(index, focus_item, focus_item, window, cx);
 822            }
 823            existing_item
 824        } else {
 825            // If the item is being opened as preview and we have an existing preview tab,
 826            // open the new item in the position of the existing preview tab.
 827            let destination_index = if allow_preview {
 828                self.close_current_preview_item(window, cx)
 829            } else {
 830                suggested_position
 831            };
 832
 833            let new_item = build_item(self, window, cx);
 834
 835            if allow_preview {
 836                self.set_preview_item_id(Some(new_item.item_id()), cx);
 837            }
 838            self.add_item_inner(
 839                new_item.clone(),
 840                true,
 841                focus_item,
 842                activate,
 843                destination_index,
 844                window,
 845                cx,
 846            );
 847
 848            new_item
 849        }
 850    }
 851
 852    pub fn close_current_preview_item(
 853        &mut self,
 854        window: &mut Window,
 855        cx: &mut Context<Self>,
 856    ) -> Option<usize> {
 857        let item_idx = self.preview_item_idx()?;
 858        let id = self.preview_item_id()?;
 859
 860        let prev_active_item_index = self.active_item_index;
 861        self.remove_item(id, false, false, window, cx);
 862        self.active_item_index = prev_active_item_index;
 863
 864        if item_idx < self.items.len() {
 865            Some(item_idx)
 866        } else {
 867            None
 868        }
 869    }
 870
 871    pub fn add_item_inner(
 872        &mut self,
 873        item: Box<dyn ItemHandle>,
 874        activate_pane: bool,
 875        focus_item: bool,
 876        activate: bool,
 877        destination_index: Option<usize>,
 878        window: &mut Window,
 879        cx: &mut Context<Self>,
 880    ) {
 881        self.close_items_over_max_tabs(window, cx);
 882
 883        if item.is_singleton(cx) {
 884            if let Some(&entry_id) = item.project_entry_ids(cx).first() {
 885                let Some(project) = self.project.upgrade() else {
 886                    return;
 887                };
 888                let project = project.read(cx);
 889                if let Some(project_path) = project.path_for_entry(entry_id, cx) {
 890                    let abs_path = project.absolute_path(&project_path, cx);
 891                    self.nav_history
 892                        .0
 893                        .lock()
 894                        .paths_by_item
 895                        .insert(item.item_id(), (project_path, abs_path));
 896                }
 897            }
 898        }
 899        // If no destination index is specified, add or move the item after the
 900        // active item (or at the start of tab bar, if the active item is pinned)
 901        let mut insertion_index = {
 902            cmp::min(
 903                if let Some(destination_index) = destination_index {
 904                    destination_index
 905                } else {
 906                    cmp::max(self.active_item_index + 1, self.pinned_count())
 907                },
 908                self.items.len(),
 909            )
 910        };
 911
 912        // Does the item already exist?
 913        let project_entry_id = if item.is_singleton(cx) {
 914            item.project_entry_ids(cx).first().copied()
 915        } else {
 916            None
 917        };
 918
 919        let existing_item_index = self.items.iter().position(|existing_item| {
 920            if existing_item.item_id() == item.item_id() {
 921                true
 922            } else if existing_item.is_singleton(cx) {
 923                existing_item
 924                    .project_entry_ids(cx)
 925                    .first()
 926                    .map_or(false, |existing_entry_id| {
 927                        Some(existing_entry_id) == project_entry_id.as_ref()
 928                    })
 929            } else {
 930                false
 931            }
 932        });
 933
 934        if let Some(existing_item_index) = existing_item_index {
 935            // If the item already exists, move it to the desired destination and activate it
 936
 937            if existing_item_index != insertion_index {
 938                let existing_item_is_active = existing_item_index == self.active_item_index;
 939
 940                // If the caller didn't specify a destination and the added item is already
 941                // the active one, don't move it
 942                if existing_item_is_active && destination_index.is_none() {
 943                    insertion_index = existing_item_index;
 944                } else {
 945                    self.items.remove(existing_item_index);
 946                    if existing_item_index < self.active_item_index {
 947                        self.active_item_index -= 1;
 948                    }
 949                    insertion_index = insertion_index.min(self.items.len());
 950
 951                    self.items.insert(insertion_index, item.clone());
 952
 953                    if existing_item_is_active {
 954                        self.active_item_index = insertion_index;
 955                    } else if insertion_index <= self.active_item_index {
 956                        self.active_item_index += 1;
 957                    }
 958                }
 959
 960                cx.notify();
 961            }
 962
 963            if activate {
 964                self.activate_item(insertion_index, activate_pane, focus_item, window, cx);
 965            }
 966        } else {
 967            self.items.insert(insertion_index, item.clone());
 968
 969            if activate {
 970                if insertion_index <= self.active_item_index
 971                    && self.preview_item_idx() != Some(self.active_item_index)
 972                {
 973                    self.active_item_index += 1;
 974                }
 975
 976                self.activate_item(insertion_index, activate_pane, focus_item, window, cx);
 977            }
 978            cx.notify();
 979        }
 980
 981        cx.emit(Event::AddItem { item });
 982    }
 983
 984    pub fn add_item(
 985        &mut self,
 986        item: Box<dyn ItemHandle>,
 987        activate_pane: bool,
 988        focus_item: bool,
 989        destination_index: Option<usize>,
 990        window: &mut Window,
 991        cx: &mut Context<Self>,
 992    ) {
 993        self.add_item_inner(
 994            item,
 995            activate_pane,
 996            focus_item,
 997            true,
 998            destination_index,
 999            window,
1000            cx,
1001        )
1002    }
1003
1004    pub fn items_len(&self) -> usize {
1005        self.items.len()
1006    }
1007
1008    pub fn items(&self) -> impl DoubleEndedIterator<Item = &Box<dyn ItemHandle>> {
1009        self.items.iter()
1010    }
1011
1012    pub fn items_of_type<T: Render>(&self) -> impl '_ + Iterator<Item = Entity<T>> {
1013        self.items
1014            .iter()
1015            .filter_map(|item| item.to_any().downcast().ok())
1016    }
1017
1018    pub fn active_item(&self) -> Option<Box<dyn ItemHandle>> {
1019        self.items.get(self.active_item_index).cloned()
1020    }
1021
1022    pub fn pixel_position_of_cursor(&self, cx: &App) -> Option<Point<Pixels>> {
1023        self.items
1024            .get(self.active_item_index)?
1025            .pixel_position_of_cursor(cx)
1026    }
1027
1028    pub fn item_for_entry(
1029        &self,
1030        entry_id: ProjectEntryId,
1031        cx: &App,
1032    ) -> Option<Box<dyn ItemHandle>> {
1033        self.items.iter().find_map(|item| {
1034            if item.is_singleton(cx) && (item.project_entry_ids(cx).as_slice() == [entry_id]) {
1035                Some(item.boxed_clone())
1036            } else {
1037                None
1038            }
1039        })
1040    }
1041
1042    pub fn item_for_path(
1043        &self,
1044        project_path: ProjectPath,
1045        cx: &App,
1046    ) -> Option<Box<dyn ItemHandle>> {
1047        self.items.iter().find_map(move |item| {
1048            if item.is_singleton(cx) && (item.project_path(cx).as_slice() == [project_path.clone()])
1049            {
1050                Some(item.boxed_clone())
1051            } else {
1052                None
1053            }
1054        })
1055    }
1056
1057    pub fn index_for_item(&self, item: &dyn ItemHandle) -> Option<usize> {
1058        self.index_for_item_id(item.item_id())
1059    }
1060
1061    fn index_for_item_id(&self, item_id: EntityId) -> Option<usize> {
1062        self.items.iter().position(|i| i.item_id() == item_id)
1063    }
1064
1065    pub fn item_for_index(&self, ix: usize) -> Option<&dyn ItemHandle> {
1066        self.items.get(ix).map(|i| i.as_ref())
1067    }
1068
1069    pub fn toggle_zoom(&mut self, _: &ToggleZoom, window: &mut Window, cx: &mut Context<Self>) {
1070        if self.zoomed {
1071            cx.emit(Event::ZoomOut);
1072        } else if !self.items.is_empty() {
1073            if !self.focus_handle.contains_focused(window, cx) {
1074                cx.focus_self(window);
1075            }
1076            cx.emit(Event::ZoomIn);
1077        }
1078    }
1079
1080    pub fn activate_item(
1081        &mut self,
1082        index: usize,
1083        activate_pane: bool,
1084        focus_item: bool,
1085        window: &mut Window,
1086        cx: &mut Context<Self>,
1087    ) {
1088        use NavigationMode::{GoingBack, GoingForward};
1089        if index < self.items.len() {
1090            let prev_active_item_ix = mem::replace(&mut self.active_item_index, index);
1091            if prev_active_item_ix != self.active_item_index
1092                || matches!(self.nav_history.mode(), GoingBack | GoingForward)
1093            {
1094                if let Some(prev_item) = self.items.get(prev_active_item_ix) {
1095                    prev_item.deactivated(window, cx);
1096                }
1097            }
1098            if let Some(newly_active_item) = self.items.get(index) {
1099                self.activation_history
1100                    .retain(|entry| entry.entity_id != newly_active_item.item_id());
1101                self.activation_history.push(ActivationHistoryEntry {
1102                    entity_id: newly_active_item.item_id(),
1103                    timestamp: self
1104                        .next_activation_timestamp
1105                        .fetch_add(1, Ordering::SeqCst),
1106                });
1107            }
1108
1109            self.update_toolbar(window, cx);
1110            self.update_status_bar(window, cx);
1111
1112            if focus_item {
1113                self.focus_active_item(window, cx);
1114            }
1115
1116            cx.emit(Event::ActivateItem {
1117                local: activate_pane,
1118                focus_changed: focus_item,
1119            });
1120
1121            if !self.is_tab_pinned(index) {
1122                self.tab_bar_scroll_handle
1123                    .scroll_to_item(index - self.pinned_tab_count);
1124            }
1125
1126            cx.notify();
1127        }
1128    }
1129
1130    pub fn activate_prev_item(
1131        &mut self,
1132        activate_pane: bool,
1133        window: &mut Window,
1134        cx: &mut Context<Self>,
1135    ) {
1136        let mut index = self.active_item_index;
1137        if index > 0 {
1138            index -= 1;
1139        } else if !self.items.is_empty() {
1140            index = self.items.len() - 1;
1141        }
1142        self.activate_item(index, activate_pane, activate_pane, window, cx);
1143    }
1144
1145    pub fn activate_next_item(
1146        &mut self,
1147        activate_pane: bool,
1148        window: &mut Window,
1149        cx: &mut Context<Self>,
1150    ) {
1151        let mut index = self.active_item_index;
1152        if index + 1 < self.items.len() {
1153            index += 1;
1154        } else {
1155            index = 0;
1156        }
1157        self.activate_item(index, activate_pane, activate_pane, window, cx);
1158    }
1159
1160    pub fn swap_item_left(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1161        let index = self.active_item_index;
1162        if index == 0 {
1163            return;
1164        }
1165
1166        self.items.swap(index, index - 1);
1167        self.activate_item(index - 1, true, true, window, cx);
1168    }
1169
1170    pub fn swap_item_right(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1171        let index = self.active_item_index;
1172        if index + 1 == self.items.len() {
1173            return;
1174        }
1175
1176        self.items.swap(index, index + 1);
1177        self.activate_item(index + 1, true, true, window, cx);
1178    }
1179
1180    pub fn close_active_item(
1181        &mut self,
1182        action: &CloseActiveItem,
1183        window: &mut Window,
1184        cx: &mut Context<Self>,
1185    ) -> Option<Task<Result<()>>> {
1186        if self.items.is_empty() {
1187            // Close the window when there's no active items to close, if configured
1188            if WorkspaceSettings::get_global(cx)
1189                .when_closing_with_no_tabs
1190                .should_close()
1191            {
1192                window.dispatch_action(Box::new(CloseWindow), cx);
1193            }
1194
1195            return None;
1196        }
1197        if self.is_tab_pinned(self.active_item_index) && !action.close_pinned {
1198            // Activate any non-pinned tab in same pane
1199            let non_pinned_tab_index = self
1200                .items()
1201                .enumerate()
1202                .find(|(index, _item)| !self.is_tab_pinned(*index))
1203                .map(|(index, _item)| index);
1204            if let Some(index) = non_pinned_tab_index {
1205                self.activate_item(index, false, false, window, cx);
1206                return None;
1207            }
1208
1209            // Activate any non-pinned tab in different pane
1210            let current_pane = cx.entity();
1211            self.workspace
1212                .update(cx, |workspace, cx| {
1213                    let panes = workspace.center.panes();
1214                    let pane_with_unpinned_tab = panes.iter().find(|pane| {
1215                        if **pane == &current_pane {
1216                            return false;
1217                        }
1218                        pane.read(cx).has_unpinned_tabs()
1219                    });
1220                    if let Some(pane) = pane_with_unpinned_tab {
1221                        pane.update(cx, |pane, cx| pane.activate_unpinned_tab(window, cx));
1222                    }
1223                })
1224                .ok();
1225
1226            return None;
1227        };
1228        let active_item_id = self.items[self.active_item_index].item_id();
1229        Some(self.close_item_by_id(
1230            active_item_id,
1231            action.save_intent.unwrap_or(SaveIntent::Close),
1232            window,
1233            cx,
1234        ))
1235    }
1236
1237    pub fn close_item_by_id(
1238        &mut self,
1239        item_id_to_close: EntityId,
1240        save_intent: SaveIntent,
1241        window: &mut Window,
1242        cx: &mut Context<Self>,
1243    ) -> Task<Result<()>> {
1244        self.close_items(window, cx, save_intent, move |view_id| {
1245            view_id == item_id_to_close
1246        })
1247    }
1248
1249    pub fn close_inactive_items(
1250        &mut self,
1251        action: &CloseInactiveItems,
1252        window: &mut Window,
1253        cx: &mut Context<Self>,
1254    ) -> Option<Task<Result<()>>> {
1255        if self.items.is_empty() {
1256            return None;
1257        }
1258
1259        let active_item_id = self.items[self.active_item_index].item_id();
1260        let non_closeable_items = self.get_non_closeable_item_ids(action.close_pinned);
1261        Some(self.close_items(
1262            window,
1263            cx,
1264            action.save_intent.unwrap_or(SaveIntent::Close),
1265            move |item_id| item_id != active_item_id && !non_closeable_items.contains(&item_id),
1266        ))
1267    }
1268
1269    pub fn close_clean_items(
1270        &mut self,
1271        action: &CloseCleanItems,
1272        window: &mut Window,
1273        cx: &mut Context<Self>,
1274    ) -> Option<Task<Result<()>>> {
1275        let item_ids: Vec<_> = self
1276            .items()
1277            .filter(|item| !item.is_dirty(cx))
1278            .map(|item| item.item_id())
1279            .collect();
1280        let non_closeable_items = self.get_non_closeable_item_ids(action.close_pinned);
1281        Some(
1282            self.close_items(window, cx, SaveIntent::Close, move |item_id| {
1283                item_ids.contains(&item_id) && !non_closeable_items.contains(&item_id)
1284            }),
1285        )
1286    }
1287
1288    pub fn close_items_to_the_left(
1289        &mut self,
1290        action: &CloseItemsToTheLeft,
1291        window: &mut Window,
1292        cx: &mut Context<Self>,
1293    ) -> Option<Task<Result<()>>> {
1294        if self.items.is_empty() {
1295            return None;
1296        }
1297        let active_item_id = self.items[self.active_item_index].item_id();
1298        let non_closeable_items = self.get_non_closeable_item_ids(action.close_pinned);
1299        Some(self.close_items_to_the_left_by_id(
1300            active_item_id,
1301            action,
1302            non_closeable_items,
1303            window,
1304            cx,
1305        ))
1306    }
1307
1308    pub fn close_items_to_the_left_by_id(
1309        &mut self,
1310        item_id: EntityId,
1311        action: &CloseItemsToTheLeft,
1312        non_closeable_items: Vec<EntityId>,
1313        window: &mut Window,
1314        cx: &mut Context<Self>,
1315    ) -> Task<Result<()>> {
1316        let item_ids: Vec<_> = self
1317            .items()
1318            .take_while(|item| item.item_id() != item_id)
1319            .map(|item| item.item_id())
1320            .collect();
1321        self.close_items(window, cx, SaveIntent::Close, move |item_id| {
1322            item_ids.contains(&item_id)
1323                && !action.close_pinned
1324                && !non_closeable_items.contains(&item_id)
1325        })
1326    }
1327
1328    pub fn close_items_to_the_right(
1329        &mut self,
1330        action: &CloseItemsToTheRight,
1331        window: &mut Window,
1332        cx: &mut Context<Self>,
1333    ) -> Option<Task<Result<()>>> {
1334        if self.items.is_empty() {
1335            return None;
1336        }
1337        let active_item_id = self.items[self.active_item_index].item_id();
1338        let non_closeable_items = self.get_non_closeable_item_ids(action.close_pinned);
1339        Some(self.close_items_to_the_right_by_id(
1340            active_item_id,
1341            action,
1342            non_closeable_items,
1343            window,
1344            cx,
1345        ))
1346    }
1347
1348    pub fn close_items_to_the_right_by_id(
1349        &mut self,
1350        item_id: EntityId,
1351        action: &CloseItemsToTheRight,
1352        non_closeable_items: Vec<EntityId>,
1353        window: &mut Window,
1354        cx: &mut Context<Self>,
1355    ) -> Task<Result<()>> {
1356        let item_ids: Vec<_> = self
1357            .items()
1358            .rev()
1359            .take_while(|item| item.item_id() != item_id)
1360            .map(|item| item.item_id())
1361            .collect();
1362        self.close_items(window, cx, SaveIntent::Close, move |item_id| {
1363            item_ids.contains(&item_id)
1364                && !action.close_pinned
1365                && !non_closeable_items.contains(&item_id)
1366        })
1367    }
1368
1369    pub fn close_all_items(
1370        &mut self,
1371        action: &CloseAllItems,
1372        window: &mut Window,
1373        cx: &mut Context<Self>,
1374    ) -> Option<Task<Result<()>>> {
1375        if self.items.is_empty() {
1376            return None;
1377        }
1378
1379        let non_closeable_items = self.get_non_closeable_item_ids(action.close_pinned);
1380        Some(self.close_items(
1381            window,
1382            cx,
1383            action.save_intent.unwrap_or(SaveIntent::Close),
1384            |item_id| !non_closeable_items.contains(&item_id),
1385        ))
1386    }
1387
1388    pub fn close_items_over_max_tabs(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1389        let Some(max_tabs) = WorkspaceSettings::get_global(cx).max_tabs.map(|i| i.get()) else {
1390            return;
1391        };
1392
1393        // Reduce over the activation history to get every dirty items up to max_tabs
1394        // count.
1395        let mut index_list = Vec::new();
1396        let mut items_len = self.items_len();
1397        let mut indexes: HashMap<EntityId, usize> = HashMap::default();
1398        for (index, item) in self.items.iter().enumerate() {
1399            indexes.insert(item.item_id(), index);
1400        }
1401        for entry in self.activation_history.iter() {
1402            if items_len < max_tabs {
1403                break;
1404            }
1405            let Some(&index) = indexes.get(&entry.entity_id) else {
1406                continue;
1407            };
1408            if let Some(true) = self.items.get(index).map(|item| item.is_dirty(cx)) {
1409                continue;
1410            }
1411
1412            index_list.push(index);
1413            items_len -= 1;
1414        }
1415        // The sort and reverse is necessary since we remove items
1416        // using their index position, hence removing from the end
1417        // of the list first to avoid changing indexes.
1418        index_list.sort_unstable();
1419        index_list
1420            .iter()
1421            .rev()
1422            .for_each(|&index| self._remove_item(index, false, false, None, window, cx));
1423    }
1424
1425    // Usually when you close an item that has unsaved changes, we prompt you to
1426    // save it. That said, if you still have the buffer open in a different pane
1427    // we can close this one without fear of losing data.
1428    pub fn skip_save_on_close(item: &dyn ItemHandle, workspace: &Workspace, cx: &App) -> bool {
1429        let mut dirty_project_item_ids = Vec::new();
1430        item.for_each_project_item(cx, &mut |project_item_id, project_item| {
1431            if project_item.is_dirty() {
1432                dirty_project_item_ids.push(project_item_id);
1433            }
1434        });
1435        if dirty_project_item_ids.is_empty() {
1436            if item.is_singleton(cx) && item.is_dirty(cx) {
1437                return false;
1438            }
1439            return true;
1440        }
1441
1442        for open_item in workspace.items(cx) {
1443            if open_item.item_id() == item.item_id() {
1444                continue;
1445            }
1446            if !open_item.is_singleton(cx) {
1447                continue;
1448            }
1449            let other_project_item_ids = open_item.project_item_model_ids(cx);
1450            dirty_project_item_ids.retain(|id| !other_project_item_ids.contains(id));
1451        }
1452        if dirty_project_item_ids.is_empty() {
1453            return true;
1454        }
1455
1456        false
1457    }
1458
1459    pub(super) fn file_names_for_prompt(
1460        items: &mut dyn Iterator<Item = &Box<dyn ItemHandle>>,
1461        cx: &App,
1462    ) -> String {
1463        let mut file_names = BTreeSet::default();
1464        for item in items {
1465            item.for_each_project_item(cx, &mut |_, project_item| {
1466                if !project_item.is_dirty() {
1467                    return;
1468                }
1469                let filename = project_item.project_path(cx).and_then(|path| {
1470                    path.path
1471                        .file_name()
1472                        .and_then(|name| name.to_str().map(ToOwned::to_owned))
1473                });
1474                file_names.insert(filename.unwrap_or("untitled".to_string()));
1475            });
1476        }
1477        if file_names.len() > 6 {
1478            format!(
1479                "{}\n.. and {} more",
1480                file_names.iter().take(5).join("\n"),
1481                file_names.len() - 5
1482            )
1483        } else {
1484            file_names.into_iter().join("\n")
1485        }
1486    }
1487
1488    pub fn close_items(
1489        &mut self,
1490        window: &mut Window,
1491        cx: &mut Context<Pane>,
1492        mut save_intent: SaveIntent,
1493        should_close: impl Fn(EntityId) -> bool,
1494    ) -> Task<Result<()>> {
1495        // Find the items to close.
1496        let mut items_to_close = Vec::new();
1497        for item in &self.items {
1498            if should_close(item.item_id()) {
1499                items_to_close.push(item.boxed_clone());
1500            }
1501        }
1502
1503        let active_item_id = self.active_item().map(|item| item.item_id());
1504
1505        items_to_close.sort_by_key(|item| {
1506            let path = item.project_path(cx);
1507            // Put the currently active item at the end, because if the currently active item is not closed last
1508            // closing the currently active item will cause the focus to switch to another item
1509            // This will cause Zed to expand the content of the currently active item
1510            //
1511            // Beyond that sort in order of project path, with untitled files and multibuffers coming last.
1512            (active_item_id == Some(item.item_id()), path.is_none(), path)
1513        });
1514
1515        let workspace = self.workspace.clone();
1516        let Some(project) = self.project.upgrade() else {
1517            return Task::ready(Ok(()));
1518        };
1519        cx.spawn_in(window, async move |pane, cx| {
1520            let dirty_items = workspace.update(cx, |workspace, cx| {
1521                items_to_close
1522                    .iter()
1523                    .filter(|item| {
1524                        item.is_dirty(cx)
1525                            && !Self::skip_save_on_close(item.as_ref(), &workspace, cx)
1526                    })
1527                    .map(|item| item.boxed_clone())
1528                    .collect::<Vec<_>>()
1529            })?;
1530
1531            if save_intent == SaveIntent::Close && dirty_items.len() > 1 {
1532                let answer = pane.update_in(cx, |_, window, cx| {
1533                    let detail = Self::file_names_for_prompt(&mut dirty_items.iter(), cx);
1534                    window.prompt(
1535                        PromptLevel::Warning,
1536                        "Do you want to save changes to the following files?",
1537                        Some(&detail),
1538                        &["Save all", "Discard all", "Cancel"],
1539                        cx,
1540                    )
1541                })?;
1542                match answer.await {
1543                    Ok(0) => save_intent = SaveIntent::SaveAll,
1544                    Ok(1) => save_intent = SaveIntent::Skip,
1545                    Ok(2) => return Ok(()),
1546                    _ => {}
1547                }
1548            }
1549
1550            for item_to_close in items_to_close {
1551                let mut should_save = true;
1552                if save_intent == SaveIntent::Close {
1553                    workspace.update(cx, |workspace, cx| {
1554                        if Self::skip_save_on_close(item_to_close.as_ref(), &workspace, cx) {
1555                            should_save = false;
1556                        }
1557                    })?;
1558                }
1559
1560                if should_save {
1561                    if !Self::save_item(project.clone(), &pane, &*item_to_close, save_intent, cx)
1562                        .await?
1563                    {
1564                        break;
1565                    }
1566                }
1567
1568                // Remove the item from the pane.
1569                pane.update_in(cx, |pane, window, cx| {
1570                    pane.remove_item(
1571                        item_to_close.item_id(),
1572                        false,
1573                        pane.close_pane_if_empty,
1574                        window,
1575                        cx,
1576                    );
1577                    pane.remove_item(item_to_close.item_id(), false, true, window, cx);
1578                })
1579                .ok();
1580            }
1581
1582            pane.update(cx, |_, cx| cx.notify()).ok();
1583            Ok(())
1584        })
1585    }
1586
1587    pub fn remove_item(
1588        &mut self,
1589        item_id: EntityId,
1590        activate_pane: bool,
1591        close_pane_if_empty: bool,
1592        window: &mut Window,
1593        cx: &mut Context<Self>,
1594    ) {
1595        let Some(item_index) = self.index_for_item_id(item_id) else {
1596            return;
1597        };
1598        self._remove_item(
1599            item_index,
1600            activate_pane,
1601            close_pane_if_empty,
1602            None,
1603            window,
1604            cx,
1605        )
1606    }
1607
1608    pub fn remove_item_and_focus_on_pane(
1609        &mut self,
1610        item_index: usize,
1611        activate_pane: bool,
1612        focus_on_pane_if_closed: Entity<Pane>,
1613        window: &mut Window,
1614        cx: &mut Context<Self>,
1615    ) {
1616        self._remove_item(
1617            item_index,
1618            activate_pane,
1619            true,
1620            Some(focus_on_pane_if_closed),
1621            window,
1622            cx,
1623        )
1624    }
1625
1626    fn _remove_item(
1627        &mut self,
1628        item_index: usize,
1629        activate_pane: bool,
1630        close_pane_if_empty: bool,
1631        focus_on_pane_if_closed: Option<Entity<Pane>>,
1632        window: &mut Window,
1633        cx: &mut Context<Self>,
1634    ) {
1635        let activate_on_close = &ItemSettings::get_global(cx).activate_on_close;
1636        self.activation_history
1637            .retain(|entry| entry.entity_id != self.items[item_index].item_id());
1638
1639        if self.is_tab_pinned(item_index) {
1640            self.pinned_tab_count -= 1;
1641        }
1642        if item_index == self.active_item_index {
1643            let left_neighbour_index = || item_index.min(self.items.len()).saturating_sub(1);
1644            let index_to_activate = match activate_on_close {
1645                ActivateOnClose::History => self
1646                    .activation_history
1647                    .pop()
1648                    .and_then(|last_activated_item| {
1649                        self.items.iter().enumerate().find_map(|(index, item)| {
1650                            (item.item_id() == last_activated_item.entity_id).then_some(index)
1651                        })
1652                    })
1653                    // We didn't have a valid activation history entry, so fallback
1654                    // to activating the item to the left
1655                    .unwrap_or_else(left_neighbour_index),
1656                ActivateOnClose::Neighbour => {
1657                    self.activation_history.pop();
1658                    if item_index + 1 < self.items.len() {
1659                        item_index + 1
1660                    } else {
1661                        item_index.saturating_sub(1)
1662                    }
1663                }
1664                ActivateOnClose::LeftNeighbour => {
1665                    self.activation_history.pop();
1666                    left_neighbour_index()
1667                }
1668            };
1669
1670            let should_activate = activate_pane || self.has_focus(window, cx);
1671            if self.items.len() == 1 && should_activate {
1672                self.focus_handle.focus(window);
1673            } else {
1674                self.activate_item(
1675                    index_to_activate,
1676                    should_activate,
1677                    should_activate,
1678                    window,
1679                    cx,
1680                );
1681            }
1682        }
1683
1684        let item = self.items.remove(item_index);
1685
1686        cx.emit(Event::RemovedItem { item: item.clone() });
1687        if self.items.is_empty() {
1688            item.deactivated(window, cx);
1689            if close_pane_if_empty {
1690                self.update_toolbar(window, cx);
1691                cx.emit(Event::Remove {
1692                    focus_on_pane: focus_on_pane_if_closed,
1693                });
1694            }
1695        }
1696
1697        if item_index < self.active_item_index {
1698            self.active_item_index -= 1;
1699        }
1700
1701        let mode = self.nav_history.mode();
1702        self.nav_history.set_mode(NavigationMode::ClosingItem);
1703        item.deactivated(window, cx);
1704        self.nav_history.set_mode(mode);
1705
1706        if self.is_active_preview_item(item.item_id()) {
1707            self.set_preview_item_id(None, cx);
1708        }
1709
1710        if let Some(path) = item.project_path(cx) {
1711            let abs_path = self
1712                .nav_history
1713                .0
1714                .lock()
1715                .paths_by_item
1716                .get(&item.item_id())
1717                .and_then(|(_, abs_path)| abs_path.clone());
1718
1719            self.nav_history
1720                .0
1721                .lock()
1722                .paths_by_item
1723                .insert(item.item_id(), (path, abs_path));
1724        } else {
1725            self.nav_history
1726                .0
1727                .lock()
1728                .paths_by_item
1729                .remove(&item.item_id());
1730        }
1731
1732        if self.zoom_out_on_close && self.items.is_empty() && close_pane_if_empty && self.zoomed {
1733            cx.emit(Event::ZoomOut);
1734        }
1735
1736        cx.notify();
1737    }
1738
1739    pub async fn save_item(
1740        project: Entity<Project>,
1741        pane: &WeakEntity<Pane>,
1742        item: &dyn ItemHandle,
1743        save_intent: SaveIntent,
1744        cx: &mut AsyncWindowContext,
1745    ) -> Result<bool> {
1746        const CONFLICT_MESSAGE: &str = "This file has changed on disk since you started editing it. Do you want to overwrite it?";
1747
1748        const DELETED_MESSAGE: &str = "This file has been deleted on disk since you started editing it. Do you want to recreate it?";
1749
1750        if save_intent == SaveIntent::Skip {
1751            return Ok(true);
1752        }
1753        let Some(item_ix) = pane
1754            .update(cx, |pane, _| pane.index_for_item(item))
1755            .ok()
1756            .flatten()
1757        else {
1758            return Ok(true);
1759        };
1760
1761        let (
1762            mut has_conflict,
1763            mut is_dirty,
1764            mut can_save,
1765            can_save_as,
1766            is_singleton,
1767            has_deleted_file,
1768        ) = cx.update(|_window, cx| {
1769            (
1770                item.has_conflict(cx),
1771                item.is_dirty(cx),
1772                item.can_save(cx),
1773                item.can_save_as(cx),
1774                item.is_singleton(cx),
1775                item.has_deleted_file(cx),
1776            )
1777        })?;
1778
1779        // when saving a single buffer, we ignore whether or not it's dirty.
1780        if save_intent == SaveIntent::Save || save_intent == SaveIntent::SaveWithoutFormat {
1781            is_dirty = true;
1782        }
1783
1784        if save_intent == SaveIntent::SaveAs {
1785            is_dirty = true;
1786            has_conflict = false;
1787            can_save = false;
1788        }
1789
1790        if save_intent == SaveIntent::Overwrite {
1791            has_conflict = false;
1792        }
1793
1794        let should_format = save_intent != SaveIntent::SaveWithoutFormat;
1795
1796        if has_conflict && can_save {
1797            if has_deleted_file && is_singleton {
1798                let answer = pane.update_in(cx, |pane, window, cx| {
1799                    pane.activate_item(item_ix, true, true, window, cx);
1800                    window.prompt(
1801                        PromptLevel::Warning,
1802                        DELETED_MESSAGE,
1803                        None,
1804                        &["Save", "Close", "Cancel"],
1805                        cx,
1806                    )
1807                })?;
1808                match answer.await {
1809                    Ok(0) => {
1810                        pane.update_in(cx, |_, window, cx| {
1811                            item.save(should_format, project, window, cx)
1812                        })?
1813                        .await?
1814                    }
1815                    Ok(1) => {
1816                        pane.update_in(cx, |pane, window, cx| {
1817                            pane.remove_item(item.item_id(), false, true, window, cx)
1818                        })?;
1819                    }
1820                    _ => return Ok(false),
1821                }
1822                return Ok(true);
1823            } else {
1824                let answer = pane.update_in(cx, |pane, window, cx| {
1825                    pane.activate_item(item_ix, true, true, window, cx);
1826                    window.prompt(
1827                        PromptLevel::Warning,
1828                        CONFLICT_MESSAGE,
1829                        None,
1830                        &["Overwrite", "Discard", "Cancel"],
1831                        cx,
1832                    )
1833                })?;
1834                match answer.await {
1835                    Ok(0) => {
1836                        pane.update_in(cx, |_, window, cx| {
1837                            item.save(should_format, project, window, cx)
1838                        })?
1839                        .await?
1840                    }
1841                    Ok(1) => {
1842                        pane.update_in(cx, |_, window, cx| item.reload(project, window, cx))?
1843                            .await?
1844                    }
1845                    _ => return Ok(false),
1846                }
1847            }
1848        } else if is_dirty && (can_save || can_save_as) {
1849            if save_intent == SaveIntent::Close {
1850                let will_autosave = cx.update(|_window, cx| {
1851                    matches!(
1852                        item.workspace_settings(cx).autosave,
1853                        AutosaveSetting::OnFocusChange | AutosaveSetting::OnWindowChange
1854                    ) && Self::can_autosave_item(item, cx)
1855                })?;
1856                if !will_autosave {
1857                    let item_id = item.item_id();
1858                    let answer_task = pane.update_in(cx, |pane, window, cx| {
1859                        if pane.save_modals_spawned.insert(item_id) {
1860                            pane.activate_item(item_ix, true, true, window, cx);
1861                            let prompt = dirty_message_for(item.project_path(cx));
1862                            Some(window.prompt(
1863                                PromptLevel::Warning,
1864                                &prompt,
1865                                None,
1866                                &["Save", "Don't Save", "Cancel"],
1867                                cx,
1868                            ))
1869                        } else {
1870                            None
1871                        }
1872                    })?;
1873                    if let Some(answer_task) = answer_task {
1874                        let answer = answer_task.await;
1875                        pane.update(cx, |pane, _| {
1876                            if !pane.save_modals_spawned.remove(&item_id) {
1877                                debug_panic!(
1878                                    "save modal was not present in spawned modals after awaiting for its answer"
1879                                )
1880                            }
1881                        })?;
1882                        match answer {
1883                            Ok(0) => {}
1884                            Ok(1) => {
1885                                // Don't save this file
1886                                pane.update_in(cx, |pane, window, cx| {
1887                                    if pane.is_tab_pinned(item_ix) && !item.can_save(cx) {
1888                                        pane.pinned_tab_count -= 1;
1889                                    }
1890                                    item.discarded(project, window, cx)
1891                                })
1892                                .log_err();
1893                                return Ok(true);
1894                            }
1895                            _ => return Ok(false), // Cancel
1896                        }
1897                    } else {
1898                        return Ok(false);
1899                    }
1900                }
1901            }
1902
1903            if can_save {
1904                pane.update_in(cx, |pane, window, cx| {
1905                    if pane.is_active_preview_item(item.item_id()) {
1906                        pane.set_preview_item_id(None, cx);
1907                    }
1908                    item.save(should_format, project, window, cx)
1909                })?
1910                .await?;
1911            } else if can_save_as && is_singleton {
1912                let abs_path = pane.update_in(cx, |pane, window, cx| {
1913                    pane.activate_item(item_ix, true, true, window, cx);
1914                    pane.workspace.update(cx, |workspace, cx| {
1915                        workspace.prompt_for_new_path(window, cx)
1916                    })
1917                })??;
1918                if let Some(abs_path) = abs_path.await.ok().flatten() {
1919                    pane.update_in(cx, |pane, window, cx| {
1920                        if let Some(item) = pane.item_for_path(abs_path.clone(), cx) {
1921                            pane.remove_item(item.item_id(), false, false, window, cx);
1922                        }
1923
1924                        item.save_as(project, abs_path, window, cx)
1925                    })?
1926                    .await?;
1927                } else {
1928                    return Ok(false);
1929                }
1930            }
1931        }
1932
1933        pane.update(cx, |_, cx| {
1934            cx.emit(Event::UserSavedItem {
1935                item: item.downgrade_item(),
1936                save_intent,
1937            });
1938            true
1939        })
1940    }
1941
1942    fn can_autosave_item(item: &dyn ItemHandle, cx: &App) -> bool {
1943        let is_deleted = item.project_entry_ids(cx).is_empty();
1944        item.is_dirty(cx) && !item.has_conflict(cx) && item.can_save(cx) && !is_deleted
1945    }
1946
1947    pub fn autosave_item(
1948        item: &dyn ItemHandle,
1949        project: Entity<Project>,
1950        window: &mut Window,
1951        cx: &mut App,
1952    ) -> Task<Result<()>> {
1953        let format = !matches!(
1954            item.workspace_settings(cx).autosave,
1955            AutosaveSetting::AfterDelay { .. }
1956        );
1957        if Self::can_autosave_item(item, cx) {
1958            item.save(format, project, window, cx)
1959        } else {
1960            Task::ready(Ok(()))
1961        }
1962    }
1963
1964    pub fn focus_active_item(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1965        if let Some(active_item) = self.active_item() {
1966            let focus_handle = active_item.item_focus_handle(cx);
1967            window.focus(&focus_handle);
1968        }
1969    }
1970
1971    pub fn split(&mut self, direction: SplitDirection, cx: &mut Context<Self>) {
1972        cx.emit(Event::Split(direction));
1973    }
1974
1975    pub fn toolbar(&self) -> &Entity<Toolbar> {
1976        &self.toolbar
1977    }
1978
1979    pub fn handle_deleted_project_item(
1980        &mut self,
1981        entry_id: ProjectEntryId,
1982        window: &mut Window,
1983        cx: &mut Context<Pane>,
1984    ) -> Option<()> {
1985        let item_id = self.items().find_map(|item| {
1986            if item.is_singleton(cx) && item.project_entry_ids(cx).as_slice() == [entry_id] {
1987                Some(item.item_id())
1988            } else {
1989                None
1990            }
1991        })?;
1992
1993        self.remove_item(item_id, false, true, window, cx);
1994        self.nav_history.remove_item(item_id);
1995
1996        Some(())
1997    }
1998
1999    fn update_toolbar(&mut self, window: &mut Window, cx: &mut Context<Self>) {
2000        let active_item = self
2001            .items
2002            .get(self.active_item_index)
2003            .map(|item| item.as_ref());
2004        self.toolbar.update(cx, |toolbar, cx| {
2005            toolbar.set_active_item(active_item, window, cx);
2006        });
2007    }
2008
2009    fn update_status_bar(&mut self, window: &mut Window, cx: &mut Context<Self>) {
2010        let workspace = self.workspace.clone();
2011        let pane = cx.entity().clone();
2012
2013        window.defer(cx, move |window, cx| {
2014            let Ok(status_bar) = workspace.update(cx, |workspace, _| workspace.status_bar.clone())
2015            else {
2016                return;
2017            };
2018
2019            status_bar.update(cx, move |status_bar, cx| {
2020                status_bar.set_active_pane(&pane, window, cx);
2021            });
2022        });
2023    }
2024
2025    fn entry_abs_path(&self, entry: ProjectEntryId, cx: &App) -> Option<PathBuf> {
2026        let worktree = self
2027            .workspace
2028            .upgrade()?
2029            .read(cx)
2030            .project()
2031            .read(cx)
2032            .worktree_for_entry(entry, cx)?
2033            .read(cx);
2034        let entry = worktree.entry_for_id(entry)?;
2035        match &entry.canonical_path {
2036            Some(canonical_path) => Some(canonical_path.to_path_buf()),
2037            None => worktree.absolutize(&entry.path).ok(),
2038        }
2039    }
2040
2041    pub fn icon_color(selected: bool) -> Color {
2042        if selected {
2043            Color::Default
2044        } else {
2045            Color::Muted
2046        }
2047    }
2048
2049    fn toggle_pin_tab(&mut self, _: &TogglePinTab, window: &mut Window, cx: &mut Context<Self>) {
2050        if self.items.is_empty() {
2051            return;
2052        }
2053        let active_tab_ix = self.active_item_index();
2054        if self.is_tab_pinned(active_tab_ix) {
2055            self.unpin_tab_at(active_tab_ix, window, cx);
2056        } else {
2057            self.pin_tab_at(active_tab_ix, window, cx);
2058        }
2059    }
2060
2061    fn pin_tab_at(&mut self, ix: usize, window: &mut Window, cx: &mut Context<Self>) {
2062        maybe!({
2063            let pane = cx.entity().clone();
2064            let destination_index = self.pinned_tab_count.min(ix);
2065            self.pinned_tab_count += 1;
2066            let id = self.item_for_index(ix)?.item_id();
2067
2068            if self.is_active_preview_item(id) {
2069                self.set_preview_item_id(None, cx);
2070            }
2071
2072            self.workspace
2073                .update(cx, |_, cx| {
2074                    cx.defer_in(window, move |_, window, cx| {
2075                        move_item(&pane, &pane, id, destination_index, window, cx)
2076                    });
2077                })
2078                .ok()?;
2079
2080            Some(())
2081        });
2082    }
2083
2084    fn unpin_tab_at(&mut self, ix: usize, window: &mut Window, cx: &mut Context<Self>) {
2085        maybe!({
2086            let pane = cx.entity().clone();
2087            self.pinned_tab_count = self.pinned_tab_count.checked_sub(1)?;
2088            let destination_index = self.pinned_tab_count;
2089
2090            let id = self.item_for_index(ix)?.item_id();
2091
2092            self.workspace
2093                .update(cx, |_, cx| {
2094                    cx.defer_in(window, move |_, window, cx| {
2095                        move_item(&pane, &pane, id, destination_index, window, cx)
2096                    });
2097                })
2098                .ok()?;
2099
2100            Some(())
2101        });
2102    }
2103
2104    fn is_tab_pinned(&self, ix: usize) -> bool {
2105        self.pinned_tab_count > ix
2106    }
2107
2108    fn has_pinned_tabs(&self) -> bool {
2109        self.pinned_tab_count != 0
2110    }
2111
2112    fn has_unpinned_tabs(&self) -> bool {
2113        self.pinned_tab_count < self.items.len()
2114    }
2115
2116    fn activate_unpinned_tab(&mut self, window: &mut Window, cx: &mut Context<Self>) {
2117        if self.items.is_empty() {
2118            return;
2119        }
2120        let Some(index) = self
2121            .items()
2122            .enumerate()
2123            .find_map(|(index, _item)| (!self.is_tab_pinned(index)).then_some(index))
2124        else {
2125            return;
2126        };
2127        self.activate_item(index, true, true, window, cx);
2128    }
2129
2130    fn render_tab(
2131        &self,
2132        ix: usize,
2133        item: &dyn ItemHandle,
2134        detail: usize,
2135        focus_handle: &FocusHandle,
2136        window: &mut Window,
2137        cx: &mut Context<Pane>,
2138    ) -> impl IntoElement + use<> {
2139        let is_active = ix == self.active_item_index;
2140        let is_preview = self
2141            .preview_item_id
2142            .map(|id| id == item.item_id())
2143            .unwrap_or(false);
2144
2145        let label = item.tab_content(
2146            TabContentParams {
2147                detail: Some(detail),
2148                selected: is_active,
2149                preview: is_preview,
2150                deemphasized: !self.has_focus(window, cx),
2151            },
2152            window,
2153            cx,
2154        );
2155
2156        let item_diagnostic = item
2157            .project_path(cx)
2158            .map_or(None, |project_path| self.diagnostics.get(&project_path));
2159
2160        let decorated_icon = item_diagnostic.map_or(None, |diagnostic| {
2161            let icon = match item.tab_icon(window, cx) {
2162                Some(icon) => icon,
2163                None => return None,
2164            };
2165
2166            let knockout_item_color = if is_active {
2167                cx.theme().colors().tab_active_background
2168            } else {
2169                cx.theme().colors().tab_bar_background
2170            };
2171
2172            let (icon_decoration, icon_color) = if matches!(diagnostic, &DiagnosticSeverity::ERROR)
2173            {
2174                (IconDecorationKind::X, Color::Error)
2175            } else {
2176                (IconDecorationKind::Triangle, Color::Warning)
2177            };
2178
2179            Some(DecoratedIcon::new(
2180                icon.size(IconSize::Small).color(Color::Muted),
2181                Some(
2182                    IconDecoration::new(icon_decoration, knockout_item_color, cx)
2183                        .color(icon_color.color(cx))
2184                        .position(Point {
2185                            x: px(-2.),
2186                            y: px(-2.),
2187                        }),
2188                ),
2189            ))
2190        });
2191
2192        let icon = if decorated_icon.is_none() {
2193            match item_diagnostic {
2194                Some(&DiagnosticSeverity::ERROR) => None,
2195                Some(&DiagnosticSeverity::WARNING) => None,
2196                _ => item
2197                    .tab_icon(window, cx)
2198                    .map(|icon| icon.color(Color::Muted)),
2199            }
2200            .map(|icon| icon.size(IconSize::Small))
2201        } else {
2202            None
2203        };
2204
2205        let settings = ItemSettings::get_global(cx);
2206        let close_side = &settings.close_position;
2207        let show_close_button = &settings.show_close_button;
2208        let indicator = render_item_indicator(item.boxed_clone(), cx);
2209        let item_id = item.item_id();
2210        let is_first_item = ix == 0;
2211        let is_last_item = ix == self.items.len() - 1;
2212        let is_pinned = self.is_tab_pinned(ix);
2213        let position_relative_to_active_item = ix.cmp(&self.active_item_index);
2214
2215        let tab = Tab::new(ix)
2216            .position(if is_first_item {
2217                TabPosition::First
2218            } else if is_last_item {
2219                TabPosition::Last
2220            } else {
2221                TabPosition::Middle(position_relative_to_active_item)
2222            })
2223            .close_side(match close_side {
2224                ClosePosition::Left => ui::TabCloseSide::Start,
2225                ClosePosition::Right => ui::TabCloseSide::End,
2226            })
2227            .toggle_state(is_active)
2228            .on_click(cx.listener(move |pane: &mut Self, _, window, cx| {
2229                pane.activate_item(ix, true, true, window, cx)
2230            }))
2231            // TODO: This should be a click listener with the middle mouse button instead of a mouse down listener.
2232            .on_mouse_down(
2233                MouseButton::Middle,
2234                cx.listener(move |pane, _event, window, cx| {
2235                    pane.close_item_by_id(item_id, SaveIntent::Close, window, cx)
2236                        .detach_and_log_err(cx);
2237                }),
2238            )
2239            .on_mouse_down(
2240                MouseButton::Left,
2241                cx.listener(move |pane, event: &MouseDownEvent, _, cx| {
2242                    if let Some(id) = pane.preview_item_id {
2243                        if id == item_id && event.click_count > 1 {
2244                            pane.set_preview_item_id(None, cx);
2245                        }
2246                    }
2247                }),
2248            )
2249            .on_drag(
2250                DraggedTab {
2251                    item: item.boxed_clone(),
2252                    pane: cx.entity().clone(),
2253                    detail,
2254                    is_active,
2255                    ix,
2256                },
2257                |tab, _, _, cx| cx.new(|_| tab.clone()),
2258            )
2259            .drag_over::<DraggedTab>(|tab, _, _, cx| {
2260                tab.bg(cx.theme().colors().drop_target_background)
2261            })
2262            .drag_over::<DraggedSelection>(|tab, _, _, cx| {
2263                tab.bg(cx.theme().colors().drop_target_background)
2264            })
2265            .when_some(self.can_drop_predicate.clone(), |this, p| {
2266                this.can_drop(move |a, window, cx| p(a, window, cx))
2267            })
2268            .on_drop(
2269                cx.listener(move |this, dragged_tab: &DraggedTab, window, cx| {
2270                    this.drag_split_direction = None;
2271                    this.handle_tab_drop(dragged_tab, ix, window, cx)
2272                }),
2273            )
2274            .on_drop(
2275                cx.listener(move |this, selection: &DraggedSelection, window, cx| {
2276                    this.drag_split_direction = None;
2277                    this.handle_dragged_selection_drop(selection, Some(ix), window, cx)
2278                }),
2279            )
2280            .on_drop(cx.listener(move |this, paths, window, cx| {
2281                this.drag_split_direction = None;
2282                this.handle_external_paths_drop(paths, window, cx)
2283            }))
2284            .when_some(item.tab_tooltip_content(cx), |tab, content| match content {
2285                TabTooltipContent::Text(text) => tab.tooltip(Tooltip::text(text.clone())),
2286                TabTooltipContent::Custom(element_fn) => {
2287                    tab.tooltip(move |window, cx| element_fn(window, cx))
2288                }
2289            })
2290            .start_slot::<Indicator>(indicator)
2291            .map(|this| {
2292                let end_slot_action: &'static dyn Action;
2293                let end_slot_tooltip_text: &'static str;
2294                let end_slot = if is_pinned {
2295                    end_slot_action = &TogglePinTab;
2296                    end_slot_tooltip_text = "Unpin Tab";
2297                    IconButton::new("unpin tab", IconName::Pin)
2298                        .shape(IconButtonShape::Square)
2299                        .icon_color(Color::Muted)
2300                        .size(ButtonSize::None)
2301                        .icon_size(IconSize::XSmall)
2302                        .on_click(cx.listener(move |pane, _, window, cx| {
2303                            pane.unpin_tab_at(ix, window, cx);
2304                        }))
2305                } else {
2306                    end_slot_action = &CloseActiveItem {
2307                        save_intent: None,
2308                        close_pinned: false,
2309                    };
2310                    end_slot_tooltip_text = "Close Tab";
2311                    match show_close_button {
2312                        ShowCloseButton::Always => IconButton::new("close tab", IconName::Close),
2313                        ShowCloseButton::Hover => {
2314                            IconButton::new("close tab", IconName::Close).visible_on_hover("")
2315                        }
2316                        ShowCloseButton::Hidden => return this,
2317                    }
2318                    .shape(IconButtonShape::Square)
2319                    .icon_color(Color::Muted)
2320                    .size(ButtonSize::None)
2321                    .icon_size(IconSize::XSmall)
2322                    .on_click(cx.listener(move |pane, _, window, cx| {
2323                        pane.close_item_by_id(item_id, SaveIntent::Close, window, cx)
2324                            .detach_and_log_err(cx);
2325                    }))
2326                }
2327                .map(|this| {
2328                    if is_active {
2329                        let focus_handle = focus_handle.clone();
2330                        this.tooltip(move |window, cx| {
2331                            Tooltip::for_action_in(
2332                                end_slot_tooltip_text,
2333                                end_slot_action,
2334                                &focus_handle,
2335                                window,
2336                                cx,
2337                            )
2338                        })
2339                    } else {
2340                        this.tooltip(Tooltip::text(end_slot_tooltip_text))
2341                    }
2342                });
2343                this.end_slot(end_slot)
2344            })
2345            .child(
2346                h_flex()
2347                    .gap_1()
2348                    .items_center()
2349                    .children(
2350                        std::iter::once(if let Some(decorated_icon) = decorated_icon {
2351                            Some(div().child(decorated_icon.into_any_element()))
2352                        } else if let Some(icon) = icon {
2353                            Some(div().child(icon.into_any_element()))
2354                        } else {
2355                            None
2356                        })
2357                        .flatten(),
2358                    )
2359                    .child(label),
2360            );
2361
2362        let single_entry_to_resolve = self.items[ix]
2363            .is_singleton(cx)
2364            .then(|| self.items[ix].project_entry_ids(cx).get(0).copied())
2365            .flatten();
2366
2367        let total_items = self.items.len();
2368        let has_items_to_left = ix > 0;
2369        let has_items_to_right = ix < total_items - 1;
2370        let is_pinned = self.is_tab_pinned(ix);
2371        let pane = cx.entity().downgrade();
2372        let menu_context = item.item_focus_handle(cx);
2373        right_click_menu(ix).trigger(tab).menu(move |window, cx| {
2374            let pane = pane.clone();
2375            let menu_context = menu_context.clone();
2376            ContextMenu::build(window, cx, move |mut menu, window, cx| {
2377                if let Some(pane) = pane.upgrade() {
2378                    menu = menu
2379                        .entry(
2380                            "Close",
2381                            Some(Box::new(CloseActiveItem {
2382                                save_intent: None,
2383                                close_pinned: true,
2384                            })),
2385                            window.handler_for(&pane, move |pane, window, cx| {
2386                                pane.close_item_by_id(item_id, SaveIntent::Close, window, cx)
2387                                    .detach_and_log_err(cx);
2388                            }),
2389                        )
2390                        .item(ContextMenuItem::Entry(
2391                            ContextMenuEntry::new("Close Others")
2392                                .action(Box::new(CloseInactiveItems {
2393                                    save_intent: None,
2394                                    close_pinned: false,
2395                                }))
2396                                .disabled(total_items == 1)
2397                                .handler(window.handler_for(&pane, move |pane, window, cx| {
2398                                    pane.close_items(window, cx, SaveIntent::Close, |id| {
2399                                        id != item_id
2400                                    })
2401                                    .detach_and_log_err(cx);
2402                                })),
2403                        ))
2404                        .separator()
2405                        .item(ContextMenuItem::Entry(
2406                            ContextMenuEntry::new("Close Left")
2407                                .action(Box::new(CloseItemsToTheLeft {
2408                                    close_pinned: false,
2409                                }))
2410                                .disabled(!has_items_to_left)
2411                                .handler(window.handler_for(&pane, move |pane, window, cx| {
2412                                    pane.close_items_to_the_left_by_id(
2413                                        item_id,
2414                                        &CloseItemsToTheLeft {
2415                                            close_pinned: false,
2416                                        },
2417                                        pane.get_non_closeable_item_ids(false),
2418                                        window,
2419                                        cx,
2420                                    )
2421                                    .detach_and_log_err(cx);
2422                                })),
2423                        ))
2424                        .item(ContextMenuItem::Entry(
2425                            ContextMenuEntry::new("Close Right")
2426                                .action(Box::new(CloseItemsToTheRight {
2427                                    close_pinned: false,
2428                                }))
2429                                .disabled(!has_items_to_right)
2430                                .handler(window.handler_for(&pane, move |pane, window, cx| {
2431                                    pane.close_items_to_the_right_by_id(
2432                                        item_id,
2433                                        &CloseItemsToTheRight {
2434                                            close_pinned: false,
2435                                        },
2436                                        pane.get_non_closeable_item_ids(false),
2437                                        window,
2438                                        cx,
2439                                    )
2440                                    .detach_and_log_err(cx);
2441                                })),
2442                        ))
2443                        .separator()
2444                        .entry(
2445                            "Close Clean",
2446                            Some(Box::new(CloseCleanItems {
2447                                close_pinned: false,
2448                            })),
2449                            window.handler_for(&pane, move |pane, window, cx| {
2450                                if let Some(task) = pane.close_clean_items(
2451                                    &CloseCleanItems {
2452                                        close_pinned: false,
2453                                    },
2454                                    window,
2455                                    cx,
2456                                ) {
2457                                    task.detach_and_log_err(cx)
2458                                }
2459                            }),
2460                        )
2461                        .entry(
2462                            "Close All",
2463                            Some(Box::new(CloseAllItems {
2464                                save_intent: None,
2465                                close_pinned: false,
2466                            })),
2467                            window.handler_for(&pane, |pane, window, cx| {
2468                                if let Some(task) = pane.close_all_items(
2469                                    &CloseAllItems {
2470                                        save_intent: None,
2471                                        close_pinned: false,
2472                                    },
2473                                    window,
2474                                    cx,
2475                                ) {
2476                                    task.detach_and_log_err(cx)
2477                                }
2478                            }),
2479                        );
2480
2481                    let pin_tab_entries = |menu: ContextMenu| {
2482                        menu.separator().map(|this| {
2483                            if is_pinned {
2484                                this.entry(
2485                                    "Unpin Tab",
2486                                    Some(TogglePinTab.boxed_clone()),
2487                                    window.handler_for(&pane, move |pane, window, cx| {
2488                                        pane.unpin_tab_at(ix, window, cx);
2489                                    }),
2490                                )
2491                            } else {
2492                                this.entry(
2493                                    "Pin Tab",
2494                                    Some(TogglePinTab.boxed_clone()),
2495                                    window.handler_for(&pane, move |pane, window, cx| {
2496                                        pane.pin_tab_at(ix, window, cx);
2497                                    }),
2498                                )
2499                            }
2500                        })
2501                    };
2502                    if let Some(entry) = single_entry_to_resolve {
2503                        let project_path = pane
2504                            .read(cx)
2505                            .item_for_entry(entry, cx)
2506                            .and_then(|item| item.project_path(cx));
2507                        let worktree = project_path.as_ref().and_then(|project_path| {
2508                            pane.read(cx)
2509                                .project
2510                                .upgrade()?
2511                                .read(cx)
2512                                .worktree_for_id(project_path.worktree_id, cx)
2513                        });
2514                        let has_relative_path = worktree.as_ref().is_some_and(|worktree| {
2515                            worktree
2516                                .read(cx)
2517                                .root_entry()
2518                                .map_or(false, |entry| entry.is_dir())
2519                        });
2520
2521                        let entry_abs_path = pane.read(cx).entry_abs_path(entry, cx);
2522                        let parent_abs_path = entry_abs_path
2523                            .as_deref()
2524                            .and_then(|abs_path| Some(abs_path.parent()?.to_path_buf()));
2525                        let relative_path = project_path
2526                            .map(|project_path| project_path.path)
2527                            .filter(|_| has_relative_path);
2528
2529                        let visible_in_project_panel = relative_path.is_some()
2530                            && worktree.is_some_and(|worktree| worktree.read(cx).is_visible());
2531
2532                        let entry_id = entry.to_proto();
2533                        menu = menu
2534                            .separator()
2535                            .when_some(entry_abs_path, |menu, abs_path| {
2536                                menu.entry(
2537                                    "Copy Path",
2538                                    Some(Box::new(zed_actions::workspace::CopyPath)),
2539                                    window.handler_for(&pane, move |_, _, cx| {
2540                                        cx.write_to_clipboard(ClipboardItem::new_string(
2541                                            abs_path.to_string_lossy().to_string(),
2542                                        ));
2543                                    }),
2544                                )
2545                            })
2546                            .when_some(relative_path, |menu, relative_path| {
2547                                menu.entry(
2548                                    "Copy Relative Path",
2549                                    Some(Box::new(zed_actions::workspace::CopyRelativePath)),
2550                                    window.handler_for(&pane, move |_, _, cx| {
2551                                        cx.write_to_clipboard(ClipboardItem::new_string(
2552                                            relative_path.to_string_lossy().to_string(),
2553                                        ));
2554                                    }),
2555                                )
2556                            })
2557                            .map(pin_tab_entries)
2558                            .separator()
2559                            .when(visible_in_project_panel, |menu| {
2560                                menu.entry(
2561                                    "Reveal In Project Panel",
2562                                    Some(Box::new(RevealInProjectPanel {
2563                                        entry_id: Some(entry_id),
2564                                    })),
2565                                    window.handler_for(&pane, move |pane, _, cx| {
2566                                        pane.project
2567                                            .update(cx, |_, cx| {
2568                                                cx.emit(project::Event::RevealInProjectPanel(
2569                                                    ProjectEntryId::from_proto(entry_id),
2570                                                ))
2571                                            })
2572                                            .ok();
2573                                    }),
2574                                )
2575                            })
2576                            .when_some(parent_abs_path, |menu, parent_abs_path| {
2577                                menu.entry(
2578                                    "Open in Terminal",
2579                                    Some(Box::new(OpenInTerminal)),
2580                                    window.handler_for(&pane, move |_, window, cx| {
2581                                        window.dispatch_action(
2582                                            OpenTerminal {
2583                                                working_directory: parent_abs_path.clone(),
2584                                            }
2585                                            .boxed_clone(),
2586                                            cx,
2587                                        );
2588                                    }),
2589                                )
2590                            });
2591                    } else {
2592                        menu = menu.map(pin_tab_entries);
2593                    }
2594                }
2595
2596                menu.context(menu_context)
2597            })
2598        })
2599    }
2600
2601    fn render_tab_bar(&mut self, window: &mut Window, cx: &mut Context<Pane>) -> AnyElement {
2602        let focus_handle = self.focus_handle.clone();
2603        let navigate_backward = IconButton::new("navigate_backward", IconName::ArrowLeft)
2604            .icon_size(IconSize::Small)
2605            .on_click({
2606                let entity = cx.entity().clone();
2607                move |_, window, cx| {
2608                    entity.update(cx, |pane, cx| pane.navigate_backward(window, cx))
2609                }
2610            })
2611            .disabled(!self.can_navigate_backward())
2612            .tooltip({
2613                let focus_handle = focus_handle.clone();
2614                move |window, cx| {
2615                    Tooltip::for_action_in("Go Back", &GoBack, &focus_handle, window, cx)
2616                }
2617            });
2618
2619        let navigate_forward = IconButton::new("navigate_forward", IconName::ArrowRight)
2620            .icon_size(IconSize::Small)
2621            .on_click({
2622                let entity = cx.entity().clone();
2623                move |_, window, cx| entity.update(cx, |pane, cx| pane.navigate_forward(window, cx))
2624            })
2625            .disabled(!self.can_navigate_forward())
2626            .tooltip({
2627                let focus_handle = focus_handle.clone();
2628                move |window, cx| {
2629                    Tooltip::for_action_in("Go Forward", &GoForward, &focus_handle, window, cx)
2630                }
2631            });
2632
2633        let mut tab_items = self
2634            .items
2635            .iter()
2636            .enumerate()
2637            .zip(tab_details(&self.items, cx))
2638            .map(|((ix, item), detail)| {
2639                self.render_tab(ix, &**item, detail, &focus_handle, window, cx)
2640            })
2641            .collect::<Vec<_>>();
2642        let tab_count = tab_items.len();
2643        let unpinned_tabs = tab_items.split_off(self.pinned_tab_count);
2644        let pinned_tabs = tab_items;
2645        TabBar::new("tab_bar")
2646            .when(
2647                self.display_nav_history_buttons.unwrap_or_default(),
2648                |tab_bar| {
2649                    tab_bar
2650                        .start_child(navigate_backward)
2651                        .start_child(navigate_forward)
2652                },
2653            )
2654            .map(|tab_bar| {
2655                if self.show_tab_bar_buttons {
2656                    let render_tab_buttons = self.render_tab_bar_buttons.clone();
2657                    let (left_children, right_children) = render_tab_buttons(self, window, cx);
2658                    tab_bar
2659                        .start_children(left_children)
2660                        .end_children(right_children)
2661                } else {
2662                    tab_bar
2663                }
2664            })
2665            .children(pinned_tabs.len().ne(&0).then(|| {
2666                let content_width = self
2667                    .tab_bar_scroll_handle
2668                    .content_size()
2669                    .map(|content_size| content_size.size.width)
2670                    .unwrap_or(px(0.));
2671                let viewport_width = self.tab_bar_scroll_handle.viewport().size.width;
2672                // We need to check both because offset returns delta values even when the scroll handle is not scrollable
2673                let is_scrollable = content_width > viewport_width;
2674                let is_scrolled = self.tab_bar_scroll_handle.offset().x < px(0.);
2675                h_flex()
2676                    .children(pinned_tabs)
2677                    .when(is_scrollable && is_scrolled, |this| {
2678                        this.border_r_1().border_color(cx.theme().colors().border)
2679                    })
2680            }))
2681            .child(
2682                h_flex()
2683                    .id("unpinned tabs")
2684                    .overflow_x_scroll()
2685                    .w_full()
2686                    .track_scroll(&self.tab_bar_scroll_handle)
2687                    .children(unpinned_tabs)
2688                    .child(
2689                        div()
2690                            .id("tab_bar_drop_target")
2691                            .min_w_6()
2692                            // HACK: This empty child is currently necessary to force the drop target to appear
2693                            // despite us setting a min width above.
2694                            .child("")
2695                            .h_full()
2696                            .flex_grow()
2697                            .drag_over::<DraggedTab>(|bar, _, _, cx| {
2698                                bar.bg(cx.theme().colors().drop_target_background)
2699                            })
2700                            .drag_over::<DraggedSelection>(|bar, _, _, cx| {
2701                                bar.bg(cx.theme().colors().drop_target_background)
2702                            })
2703                            .on_drop(cx.listener(
2704                                move |this, dragged_tab: &DraggedTab, window, cx| {
2705                                    this.drag_split_direction = None;
2706                                    this.handle_tab_drop(dragged_tab, this.items.len(), window, cx)
2707                                },
2708                            ))
2709                            .on_drop(cx.listener(
2710                                move |this, selection: &DraggedSelection, window, cx| {
2711                                    this.drag_split_direction = None;
2712                                    this.handle_project_entry_drop(
2713                                        &selection.active_selection.entry_id,
2714                                        Some(tab_count),
2715                                        window,
2716                                        cx,
2717                                    )
2718                                },
2719                            ))
2720                            .on_drop(cx.listener(move |this, paths, window, cx| {
2721                                this.drag_split_direction = None;
2722                                this.handle_external_paths_drop(paths, window, cx)
2723                            }))
2724                            .on_click(cx.listener(move |this, event: &ClickEvent, window, cx| {
2725                                if event.up.click_count == 2 {
2726                                    window.dispatch_action(
2727                                        this.double_click_dispatch_action.boxed_clone(),
2728                                        cx,
2729                                    );
2730                                }
2731                            })),
2732                    ),
2733            )
2734            .into_any_element()
2735    }
2736
2737    pub fn render_menu_overlay(menu: &Entity<ContextMenu>) -> Div {
2738        div().absolute().bottom_0().right_0().size_0().child(
2739            deferred(anchored().anchor(Corner::TopRight).child(menu.clone())).with_priority(1),
2740        )
2741    }
2742
2743    pub fn set_zoomed(&mut self, zoomed: bool, cx: &mut Context<Self>) {
2744        self.zoomed = zoomed;
2745        cx.notify();
2746    }
2747
2748    pub fn is_zoomed(&self) -> bool {
2749        self.zoomed
2750    }
2751
2752    fn handle_drag_move<T: 'static>(
2753        &mut self,
2754        event: &DragMoveEvent<T>,
2755        window: &mut Window,
2756        cx: &mut Context<Self>,
2757    ) {
2758        let can_split_predicate = self.can_split_predicate.take();
2759        let can_split = match &can_split_predicate {
2760            Some(can_split_predicate) => {
2761                can_split_predicate(self, event.dragged_item(), window, cx)
2762            }
2763            None => false,
2764        };
2765        self.can_split_predicate = can_split_predicate;
2766        if !can_split {
2767            return;
2768        }
2769
2770        let rect = event.bounds.size;
2771
2772        let size = event.bounds.size.width.min(event.bounds.size.height)
2773            * WorkspaceSettings::get_global(cx).drop_target_size;
2774
2775        let relative_cursor = Point::new(
2776            event.event.position.x - event.bounds.left(),
2777            event.event.position.y - event.bounds.top(),
2778        );
2779
2780        let direction = if relative_cursor.x < size
2781            || relative_cursor.x > rect.width - size
2782            || relative_cursor.y < size
2783            || relative_cursor.y > rect.height - size
2784        {
2785            [
2786                SplitDirection::Up,
2787                SplitDirection::Right,
2788                SplitDirection::Down,
2789                SplitDirection::Left,
2790            ]
2791            .iter()
2792            .min_by_key(|side| match side {
2793                SplitDirection::Up => relative_cursor.y,
2794                SplitDirection::Right => rect.width - relative_cursor.x,
2795                SplitDirection::Down => rect.height - relative_cursor.y,
2796                SplitDirection::Left => relative_cursor.x,
2797            })
2798            .cloned()
2799        } else {
2800            None
2801        };
2802
2803        if direction != self.drag_split_direction {
2804            self.drag_split_direction = direction;
2805        }
2806    }
2807
2808    pub fn handle_tab_drop(
2809        &mut self,
2810        dragged_tab: &DraggedTab,
2811        ix: usize,
2812        window: &mut Window,
2813        cx: &mut Context<Self>,
2814    ) {
2815        if let Some(custom_drop_handle) = self.custom_drop_handle.clone() {
2816            if let ControlFlow::Break(()) = custom_drop_handle(self, dragged_tab, window, cx) {
2817                return;
2818            }
2819        }
2820        let mut to_pane = cx.entity().clone();
2821        let split_direction = self.drag_split_direction;
2822        let item_id = dragged_tab.item.item_id();
2823        if let Some(preview_item_id) = self.preview_item_id {
2824            if item_id == preview_item_id {
2825                self.set_preview_item_id(None, cx);
2826            }
2827        }
2828
2829        let from_pane = dragged_tab.pane.clone();
2830        self.workspace
2831            .update(cx, |_, cx| {
2832                cx.defer_in(window, move |workspace, window, cx| {
2833                    if let Some(split_direction) = split_direction {
2834                        to_pane = workspace.split_pane(to_pane, split_direction, window, cx);
2835                    }
2836                    let old_ix = from_pane.read(cx).index_for_item_id(item_id);
2837                    let old_len = to_pane.read(cx).items.len();
2838                    move_item(&from_pane, &to_pane, item_id, ix, window, cx);
2839                    if to_pane == from_pane {
2840                        if let Some(old_index) = old_ix {
2841                            to_pane.update(cx, |this, _| {
2842                                if old_index < this.pinned_tab_count
2843                                    && (ix == this.items.len() || ix > this.pinned_tab_count)
2844                                {
2845                                    this.pinned_tab_count -= 1;
2846                                } else if this.has_pinned_tabs()
2847                                    && old_index >= this.pinned_tab_count
2848                                    && ix < this.pinned_tab_count
2849                                {
2850                                    this.pinned_tab_count += 1;
2851                                }
2852                            });
2853                        }
2854                    } else {
2855                        to_pane.update(cx, |this, _| {
2856                            if this.items.len() > old_len // Did we not deduplicate on drag?
2857                                && this.has_pinned_tabs()
2858                                && ix < this.pinned_tab_count
2859                            {
2860                                this.pinned_tab_count += 1;
2861                            }
2862                        });
2863                        from_pane.update(cx, |this, _| {
2864                            if let Some(index) = old_ix {
2865                                if this.pinned_tab_count > index {
2866                                    this.pinned_tab_count -= 1;
2867                                }
2868                            }
2869                        })
2870                    }
2871                });
2872            })
2873            .log_err();
2874    }
2875
2876    fn handle_dragged_selection_drop(
2877        &mut self,
2878        dragged_selection: &DraggedSelection,
2879        dragged_onto: Option<usize>,
2880        window: &mut Window,
2881        cx: &mut Context<Self>,
2882    ) {
2883        if let Some(custom_drop_handle) = self.custom_drop_handle.clone() {
2884            if let ControlFlow::Break(()) = custom_drop_handle(self, dragged_selection, window, cx)
2885            {
2886                return;
2887            }
2888        }
2889        self.handle_project_entry_drop(
2890            &dragged_selection.active_selection.entry_id,
2891            dragged_onto,
2892            window,
2893            cx,
2894        );
2895    }
2896
2897    fn handle_project_entry_drop(
2898        &mut self,
2899        project_entry_id: &ProjectEntryId,
2900        target: Option<usize>,
2901        window: &mut Window,
2902        cx: &mut Context<Self>,
2903    ) {
2904        if let Some(custom_drop_handle) = self.custom_drop_handle.clone() {
2905            if let ControlFlow::Break(()) = custom_drop_handle(self, project_entry_id, window, cx) {
2906                return;
2907            }
2908        }
2909        let mut to_pane = cx.entity().clone();
2910        let split_direction = self.drag_split_direction;
2911        let project_entry_id = *project_entry_id;
2912        self.workspace
2913            .update(cx, |_, cx| {
2914                cx.defer_in(window, move |workspace, window, cx| {
2915                    if let Some(path) = workspace
2916                        .project()
2917                        .read(cx)
2918                        .path_for_entry(project_entry_id, cx)
2919                    {
2920                        let load_path_task = workspace.load_path(path, window, cx);
2921                        cx.spawn_in(window, async move |workspace, cx| {
2922                            if let Some((project_entry_id, build_item)) =
2923                                load_path_task.await.notify_async_err(cx)
2924                            {
2925                                let (to_pane, new_item_handle) = workspace
2926                                    .update_in(cx, |workspace, window, cx| {
2927                                        if let Some(split_direction) = split_direction {
2928                                            to_pane = workspace.split_pane(
2929                                                to_pane,
2930                                                split_direction,
2931                                                window,
2932                                                cx,
2933                                            );
2934                                        }
2935                                        let new_item_handle = to_pane.update(cx, |pane, cx| {
2936                                            pane.open_item(
2937                                                project_entry_id,
2938                                                true,
2939                                                false,
2940                                                true,
2941                                                target,
2942                                                window,
2943                                                cx,
2944                                                build_item,
2945                                            )
2946                                        });
2947                                        (to_pane, new_item_handle)
2948                                    })
2949                                    .log_err()?;
2950                                to_pane
2951                                    .update_in(cx, |this, window, cx| {
2952                                        let Some(index) = this.index_for_item(&*new_item_handle)
2953                                        else {
2954                                            return;
2955                                        };
2956
2957                                        if target.map_or(false, |target| this.is_tab_pinned(target))
2958                                        {
2959                                            this.pin_tab_at(index, window, cx);
2960                                        }
2961                                    })
2962                                    .ok()?
2963                            }
2964                            Some(())
2965                        })
2966                        .detach();
2967                    };
2968                });
2969            })
2970            .log_err();
2971    }
2972
2973    fn handle_external_paths_drop(
2974        &mut self,
2975        paths: &ExternalPaths,
2976        window: &mut Window,
2977        cx: &mut Context<Self>,
2978    ) {
2979        if let Some(custom_drop_handle) = self.custom_drop_handle.clone() {
2980            if let ControlFlow::Break(()) = custom_drop_handle(self, paths, window, cx) {
2981                return;
2982            }
2983        }
2984        let mut to_pane = cx.entity().clone();
2985        let mut split_direction = self.drag_split_direction;
2986        let paths = paths.paths().to_vec();
2987        let is_remote = self
2988            .workspace
2989            .update(cx, |workspace, cx| {
2990                if workspace.project().read(cx).is_via_collab() {
2991                    workspace.show_error(
2992                        &anyhow::anyhow!("Cannot drop files on a remote project"),
2993                        cx,
2994                    );
2995                    true
2996                } else {
2997                    false
2998                }
2999            })
3000            .unwrap_or(true);
3001        if is_remote {
3002            return;
3003        }
3004
3005        self.workspace
3006            .update(cx, |workspace, cx| {
3007                let fs = Arc::clone(workspace.project().read(cx).fs());
3008                cx.spawn_in(window, async move |workspace, cx| {
3009                    let mut is_file_checks = FuturesUnordered::new();
3010                    for path in &paths {
3011                        is_file_checks.push(fs.is_file(path))
3012                    }
3013                    let mut has_files_to_open = false;
3014                    while let Some(is_file) = is_file_checks.next().await {
3015                        if is_file {
3016                            has_files_to_open = true;
3017                            break;
3018                        }
3019                    }
3020                    drop(is_file_checks);
3021                    if !has_files_to_open {
3022                        split_direction = None;
3023                    }
3024
3025                    if let Ok(open_task) = workspace.update_in(cx, |workspace, window, cx| {
3026                        if let Some(split_direction) = split_direction {
3027                            to_pane = workspace.split_pane(to_pane, split_direction, window, cx);
3028                        }
3029                        workspace.open_paths(
3030                            paths,
3031                            OpenOptions {
3032                                visible: Some(OpenVisible::OnlyDirectories),
3033                                ..Default::default()
3034                            },
3035                            Some(to_pane.downgrade()),
3036                            window,
3037                            cx,
3038                        )
3039                    }) {
3040                        let opened_items: Vec<_> = open_task.await;
3041                        _ = workspace.update(cx, |workspace, cx| {
3042                            for item in opened_items.into_iter().flatten() {
3043                                if let Err(e) = item {
3044                                    workspace.show_error(&e, cx);
3045                                }
3046                            }
3047                        });
3048                    }
3049                })
3050                .detach();
3051            })
3052            .log_err();
3053    }
3054
3055    pub fn display_nav_history_buttons(&mut self, display: Option<bool>) {
3056        self.display_nav_history_buttons = display;
3057    }
3058
3059    fn get_non_closeable_item_ids(&self, close_pinned: bool) -> Vec<EntityId> {
3060        if close_pinned {
3061            return vec![];
3062        }
3063
3064        self.items
3065            .iter()
3066            .enumerate()
3067            .filter(|(index, _item)| self.is_tab_pinned(*index))
3068            .map(|(_, item)| item.item_id())
3069            .collect()
3070    }
3071
3072    pub fn drag_split_direction(&self) -> Option<SplitDirection> {
3073        self.drag_split_direction
3074    }
3075
3076    pub fn set_zoom_out_on_close(&mut self, zoom_out_on_close: bool) {
3077        self.zoom_out_on_close = zoom_out_on_close;
3078    }
3079}
3080
3081fn default_render_tab_bar_buttons(
3082    pane: &mut Pane,
3083    window: &mut Window,
3084    cx: &mut Context<Pane>,
3085) -> (Option<AnyElement>, Option<AnyElement>) {
3086    if !pane.has_focus(window, cx) && !pane.context_menu_focused(window, cx) {
3087        return (None, None);
3088    }
3089    // Ideally we would return a vec of elements here to pass directly to the [TabBar]'s
3090    // `end_slot`, but due to needing a view here that isn't possible.
3091    let right_children = h_flex()
3092        // Instead we need to replicate the spacing from the [TabBar]'s `end_slot` here.
3093        .gap(DynamicSpacing::Base04.rems(cx))
3094        .child(
3095            PopoverMenu::new("pane-tab-bar-popover-menu")
3096                .trigger_with_tooltip(
3097                    IconButton::new("plus", IconName::Plus).icon_size(IconSize::Small),
3098                    Tooltip::text("New..."),
3099                )
3100                .anchor(Corner::TopRight)
3101                .with_handle(pane.new_item_context_menu_handle.clone())
3102                .menu(move |window, cx| {
3103                    Some(ContextMenu::build(window, cx, |menu, _, _| {
3104                        menu.action("New File", NewFile.boxed_clone())
3105                            .action("Open File", ToggleFileFinder::default().boxed_clone())
3106                            .separator()
3107                            .action(
3108                                "Search Project",
3109                                DeploySearch {
3110                                    replace_enabled: false,
3111                                }
3112                                .boxed_clone(),
3113                            )
3114                            .action("Search Symbols", ToggleProjectSymbols.boxed_clone())
3115                            .separator()
3116                            .action("New Terminal", NewTerminal.boxed_clone())
3117                    }))
3118                }),
3119        )
3120        .child(
3121            PopoverMenu::new("pane-tab-bar-split")
3122                .trigger_with_tooltip(
3123                    IconButton::new("split", IconName::Split).icon_size(IconSize::Small),
3124                    Tooltip::text("Split Pane"),
3125                )
3126                .anchor(Corner::TopRight)
3127                .with_handle(pane.split_item_context_menu_handle.clone())
3128                .menu(move |window, cx| {
3129                    ContextMenu::build(window, cx, |menu, _, _| {
3130                        menu.action("Split Right", SplitRight.boxed_clone())
3131                            .action("Split Left", SplitLeft.boxed_clone())
3132                            .action("Split Up", SplitUp.boxed_clone())
3133                            .action("Split Down", SplitDown.boxed_clone())
3134                    })
3135                    .into()
3136                }),
3137        )
3138        .child({
3139            let zoomed = pane.is_zoomed();
3140            IconButton::new("toggle_zoom", IconName::Maximize)
3141                .icon_size(IconSize::Small)
3142                .toggle_state(zoomed)
3143                .selected_icon(IconName::Minimize)
3144                .on_click(cx.listener(|pane, _, window, cx| {
3145                    pane.toggle_zoom(&crate::ToggleZoom, window, cx);
3146                }))
3147                .tooltip(move |window, cx| {
3148                    Tooltip::for_action(
3149                        if zoomed { "Zoom Out" } else { "Zoom In" },
3150                        &ToggleZoom,
3151                        window,
3152                        cx,
3153                    )
3154                })
3155        })
3156        .into_any_element()
3157        .into();
3158    (None, right_children)
3159}
3160
3161impl Focusable for Pane {
3162    fn focus_handle(&self, _cx: &App) -> FocusHandle {
3163        self.focus_handle.clone()
3164    }
3165}
3166
3167impl Render for Pane {
3168    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
3169        let mut key_context = KeyContext::new_with_defaults();
3170        key_context.add("Pane");
3171        if self.active_item().is_none() {
3172            key_context.add("EmptyPane");
3173        }
3174
3175        let should_display_tab_bar = self.should_display_tab_bar.clone();
3176        let display_tab_bar = should_display_tab_bar(window, cx);
3177        let Some(project) = self.project.upgrade() else {
3178            return div().track_focus(&self.focus_handle(cx));
3179        };
3180        let is_local = project.read(cx).is_local();
3181
3182        v_flex()
3183            .key_context(key_context)
3184            .track_focus(&self.focus_handle(cx))
3185            .size_full()
3186            .flex_none()
3187            .overflow_hidden()
3188            .on_action(cx.listener(|pane, _: &AlternateFile, window, cx| {
3189                pane.alternate_file(window, cx);
3190            }))
3191            .on_action(
3192                cx.listener(|pane, _: &SplitLeft, _, cx| pane.split(SplitDirection::Left, cx)),
3193            )
3194            .on_action(cx.listener(|pane, _: &SplitUp, _, cx| pane.split(SplitDirection::Up, cx)))
3195            .on_action(cx.listener(|pane, _: &SplitHorizontal, _, cx| {
3196                pane.split(SplitDirection::horizontal(cx), cx)
3197            }))
3198            .on_action(cx.listener(|pane, _: &SplitVertical, _, cx| {
3199                pane.split(SplitDirection::vertical(cx), cx)
3200            }))
3201            .on_action(
3202                cx.listener(|pane, _: &SplitRight, _, cx| pane.split(SplitDirection::Right, cx)),
3203            )
3204            .on_action(
3205                cx.listener(|pane, _: &SplitDown, _, cx| pane.split(SplitDirection::Down, cx)),
3206            )
3207            .on_action(
3208                cx.listener(|pane, _: &GoBack, window, cx| pane.navigate_backward(window, cx)),
3209            )
3210            .on_action(
3211                cx.listener(|pane, _: &GoForward, window, cx| pane.navigate_forward(window, cx)),
3212            )
3213            .on_action(cx.listener(|_, _: &JoinIntoNext, _, cx| {
3214                cx.emit(Event::JoinIntoNext);
3215            }))
3216            .on_action(cx.listener(|_, _: &JoinAll, _, cx| {
3217                cx.emit(Event::JoinAll);
3218            }))
3219            .on_action(cx.listener(Pane::toggle_zoom))
3220            .on_action(
3221                cx.listener(|pane: &mut Pane, action: &ActivateItem, window, cx| {
3222                    pane.activate_item(action.0, true, true, window, cx);
3223                }),
3224            )
3225            .on_action(
3226                cx.listener(|pane: &mut Pane, _: &ActivateLastItem, window, cx| {
3227                    pane.activate_item(pane.items.len() - 1, true, true, window, cx);
3228                }),
3229            )
3230            .on_action(
3231                cx.listener(|pane: &mut Pane, _: &ActivatePreviousItem, window, cx| {
3232                    pane.activate_prev_item(true, window, cx);
3233                }),
3234            )
3235            .on_action(
3236                cx.listener(|pane: &mut Pane, _: &ActivateNextItem, window, cx| {
3237                    pane.activate_next_item(true, window, cx);
3238                }),
3239            )
3240            .on_action(
3241                cx.listener(|pane, _: &SwapItemLeft, window, cx| pane.swap_item_left(window, cx)),
3242            )
3243            .on_action(
3244                cx.listener(|pane, _: &SwapItemRight, window, cx| pane.swap_item_right(window, cx)),
3245            )
3246            .on_action(cx.listener(|pane, action, window, cx| {
3247                pane.toggle_pin_tab(action, window, cx);
3248            }))
3249            .when(PreviewTabsSettings::get_global(cx).enabled, |this| {
3250                this.on_action(cx.listener(|pane: &mut Pane, _: &TogglePreviewTab, _, cx| {
3251                    if let Some(active_item_id) = pane.active_item().map(|i| i.item_id()) {
3252                        if pane.is_active_preview_item(active_item_id) {
3253                            pane.set_preview_item_id(None, cx);
3254                        } else {
3255                            pane.set_preview_item_id(Some(active_item_id), cx);
3256                        }
3257                    }
3258                }))
3259            })
3260            .on_action(
3261                cx.listener(|pane: &mut Self, action: &CloseActiveItem, window, cx| {
3262                    if let Some(task) = pane.close_active_item(action, window, cx) {
3263                        task.detach_and_log_err(cx)
3264                    }
3265                }),
3266            )
3267            .on_action(
3268                cx.listener(|pane: &mut Self, action: &CloseInactiveItems, window, cx| {
3269                    if let Some(task) = pane.close_inactive_items(action, window, cx) {
3270                        task.detach_and_log_err(cx)
3271                    }
3272                }),
3273            )
3274            .on_action(
3275                cx.listener(|pane: &mut Self, action: &CloseCleanItems, window, cx| {
3276                    if let Some(task) = pane.close_clean_items(action, window, cx) {
3277                        task.detach_and_log_err(cx)
3278                    }
3279                }),
3280            )
3281            .on_action(cx.listener(
3282                |pane: &mut Self, action: &CloseItemsToTheLeft, window, cx| {
3283                    if let Some(task) = pane.close_items_to_the_left(action, window, cx) {
3284                        task.detach_and_log_err(cx)
3285                    }
3286                },
3287            ))
3288            .on_action(cx.listener(
3289                |pane: &mut Self, action: &CloseItemsToTheRight, window, cx| {
3290                    if let Some(task) = pane.close_items_to_the_right(action, window, cx) {
3291                        task.detach_and_log_err(cx)
3292                    }
3293                },
3294            ))
3295            .on_action(
3296                cx.listener(|pane: &mut Self, action: &CloseAllItems, window, cx| {
3297                    if let Some(task) = pane.close_all_items(action, window, cx) {
3298                        task.detach_and_log_err(cx)
3299                    }
3300                }),
3301            )
3302            .on_action(
3303                cx.listener(|pane: &mut Self, action: &CloseActiveItem, window, cx| {
3304                    if let Some(task) = pane.close_active_item(action, window, cx) {
3305                        task.detach_and_log_err(cx)
3306                    }
3307                }),
3308            )
3309            .on_action(
3310                cx.listener(|pane: &mut Self, action: &RevealInProjectPanel, _, cx| {
3311                    let entry_id = action
3312                        .entry_id
3313                        .map(ProjectEntryId::from_proto)
3314                        .or_else(|| pane.active_item()?.project_entry_ids(cx).first().copied());
3315                    if let Some(entry_id) = entry_id {
3316                        pane.project
3317                            .update(cx, |_, cx| {
3318                                cx.emit(project::Event::RevealInProjectPanel(entry_id))
3319                            })
3320                            .ok();
3321                    }
3322                }),
3323            )
3324            .when(self.active_item().is_some() && display_tab_bar, |pane| {
3325                pane.child((self.render_tab_bar.clone())(self, window, cx))
3326            })
3327            .child({
3328                let has_worktrees = project.read(cx).visible_worktrees(cx).next().is_some();
3329                // main content
3330                div()
3331                    .flex_1()
3332                    .relative()
3333                    .group("")
3334                    .overflow_hidden()
3335                    .on_drag_move::<DraggedTab>(cx.listener(Self::handle_drag_move))
3336                    .on_drag_move::<DraggedSelection>(cx.listener(Self::handle_drag_move))
3337                    .when(is_local, |div| {
3338                        div.on_drag_move::<ExternalPaths>(cx.listener(Self::handle_drag_move))
3339                    })
3340                    .map(|div| {
3341                        if let Some(item) = self.active_item() {
3342                            div.id("pane_placeholder")
3343                                .v_flex()
3344                                .size_full()
3345                                .overflow_hidden()
3346                                .child(self.toolbar.clone())
3347                                .child(item.to_any())
3348                        } else {
3349                            let placeholder = div
3350                                .id("pane_placeholder")
3351                                .h_flex()
3352                                .size_full()
3353                                .justify_center()
3354                                .on_click(cx.listener(
3355                                    move |this, event: &ClickEvent, window, cx| {
3356                                        if event.up.click_count == 2 {
3357                                            window.dispatch_action(
3358                                                this.double_click_dispatch_action.boxed_clone(),
3359                                                cx,
3360                                            );
3361                                        }
3362                                    },
3363                                ));
3364                            if has_worktrees {
3365                                placeholder
3366                            } else {
3367                                placeholder.child(
3368                                    Label::new("Open a file or project to get started.")
3369                                        .color(Color::Muted),
3370                                )
3371                            }
3372                        }
3373                    })
3374                    .child(
3375                        // drag target
3376                        div()
3377                            .invisible()
3378                            .absolute()
3379                            .bg(cx.theme().colors().drop_target_background)
3380                            .group_drag_over::<DraggedTab>("", |style| style.visible())
3381                            .group_drag_over::<DraggedSelection>("", |style| style.visible())
3382                            .when(is_local, |div| {
3383                                div.group_drag_over::<ExternalPaths>("", |style| style.visible())
3384                            })
3385                            .when_some(self.can_drop_predicate.clone(), |this, p| {
3386                                this.can_drop(move |a, window, cx| p(a, window, cx))
3387                            })
3388                            .on_drop(cx.listener(move |this, dragged_tab, window, cx| {
3389                                this.handle_tab_drop(
3390                                    dragged_tab,
3391                                    this.active_item_index(),
3392                                    window,
3393                                    cx,
3394                                )
3395                            }))
3396                            .on_drop(cx.listener(
3397                                move |this, selection: &DraggedSelection, window, cx| {
3398                                    this.handle_dragged_selection_drop(selection, None, window, cx)
3399                                },
3400                            ))
3401                            .on_drop(cx.listener(move |this, paths, window, cx| {
3402                                this.handle_external_paths_drop(paths, window, cx)
3403                            }))
3404                            .map(|div| {
3405                                let size = DefiniteLength::Fraction(0.5);
3406                                match self.drag_split_direction {
3407                                    None => div.top_0().right_0().bottom_0().left_0(),
3408                                    Some(SplitDirection::Up) => {
3409                                        div.top_0().left_0().right_0().h(size)
3410                                    }
3411                                    Some(SplitDirection::Down) => {
3412                                        div.left_0().bottom_0().right_0().h(size)
3413                                    }
3414                                    Some(SplitDirection::Left) => {
3415                                        div.top_0().left_0().bottom_0().w(size)
3416                                    }
3417                                    Some(SplitDirection::Right) => {
3418                                        div.top_0().bottom_0().right_0().w(size)
3419                                    }
3420                                }
3421                            }),
3422                    )
3423            })
3424            .on_mouse_down(
3425                MouseButton::Navigate(NavigationDirection::Back),
3426                cx.listener(|pane, _, window, cx| {
3427                    if let Some(workspace) = pane.workspace.upgrade() {
3428                        let pane = cx.entity().downgrade();
3429                        window.defer(cx, move |window, cx| {
3430                            workspace.update(cx, |workspace, cx| {
3431                                workspace.go_back(pane, window, cx).detach_and_log_err(cx)
3432                            })
3433                        })
3434                    }
3435                }),
3436            )
3437            .on_mouse_down(
3438                MouseButton::Navigate(NavigationDirection::Forward),
3439                cx.listener(|pane, _, window, cx| {
3440                    if let Some(workspace) = pane.workspace.upgrade() {
3441                        let pane = cx.entity().downgrade();
3442                        window.defer(cx, move |window, cx| {
3443                            workspace.update(cx, |workspace, cx| {
3444                                workspace
3445                                    .go_forward(pane, window, cx)
3446                                    .detach_and_log_err(cx)
3447                            })
3448                        })
3449                    }
3450                }),
3451            )
3452    }
3453}
3454
3455impl ItemNavHistory {
3456    pub fn push<D: 'static + Send + Any>(&mut self, data: Option<D>, cx: &mut App) {
3457        if self
3458            .item
3459            .upgrade()
3460            .is_some_and(|item| item.include_in_nav_history())
3461        {
3462            self.history
3463                .push(data, self.item.clone(), self.is_preview, cx);
3464        }
3465    }
3466
3467    pub fn pop_backward(&mut self, cx: &mut App) -> Option<NavigationEntry> {
3468        self.history.pop(NavigationMode::GoingBack, cx)
3469    }
3470
3471    pub fn pop_forward(&mut self, cx: &mut App) -> Option<NavigationEntry> {
3472        self.history.pop(NavigationMode::GoingForward, cx)
3473    }
3474}
3475
3476impl NavHistory {
3477    pub fn for_each_entry(
3478        &self,
3479        cx: &App,
3480        mut f: impl FnMut(&NavigationEntry, (ProjectPath, Option<PathBuf>)),
3481    ) {
3482        let borrowed_history = self.0.lock();
3483        borrowed_history
3484            .forward_stack
3485            .iter()
3486            .chain(borrowed_history.backward_stack.iter())
3487            .chain(borrowed_history.closed_stack.iter())
3488            .for_each(|entry| {
3489                if let Some(project_and_abs_path) =
3490                    borrowed_history.paths_by_item.get(&entry.item.id())
3491                {
3492                    f(entry, project_and_abs_path.clone());
3493                } else if let Some(item) = entry.item.upgrade() {
3494                    if let Some(path) = item.project_path(cx) {
3495                        f(entry, (path, None));
3496                    }
3497                }
3498            })
3499    }
3500
3501    pub fn set_mode(&mut self, mode: NavigationMode) {
3502        self.0.lock().mode = mode;
3503    }
3504
3505    pub fn mode(&self) -> NavigationMode {
3506        self.0.lock().mode
3507    }
3508
3509    pub fn disable(&mut self) {
3510        self.0.lock().mode = NavigationMode::Disabled;
3511    }
3512
3513    pub fn enable(&mut self) {
3514        self.0.lock().mode = NavigationMode::Normal;
3515    }
3516
3517    pub fn pop(&mut self, mode: NavigationMode, cx: &mut App) -> Option<NavigationEntry> {
3518        let mut state = self.0.lock();
3519        let entry = match mode {
3520            NavigationMode::Normal | NavigationMode::Disabled | NavigationMode::ClosingItem => {
3521                return None;
3522            }
3523            NavigationMode::GoingBack => &mut state.backward_stack,
3524            NavigationMode::GoingForward => &mut state.forward_stack,
3525            NavigationMode::ReopeningClosedItem => &mut state.closed_stack,
3526        }
3527        .pop_back();
3528        if entry.is_some() {
3529            state.did_update(cx);
3530        }
3531        entry
3532    }
3533
3534    pub fn push<D: 'static + Send + Any>(
3535        &mut self,
3536        data: Option<D>,
3537        item: Arc<dyn WeakItemHandle>,
3538        is_preview: bool,
3539        cx: &mut App,
3540    ) {
3541        let state = &mut *self.0.lock();
3542        match state.mode {
3543            NavigationMode::Disabled => {}
3544            NavigationMode::Normal | NavigationMode::ReopeningClosedItem => {
3545                if state.backward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
3546                    state.backward_stack.pop_front();
3547                }
3548                state.backward_stack.push_back(NavigationEntry {
3549                    item,
3550                    data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
3551                    timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
3552                    is_preview,
3553                });
3554                state.forward_stack.clear();
3555            }
3556            NavigationMode::GoingBack => {
3557                if state.forward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
3558                    state.forward_stack.pop_front();
3559                }
3560                state.forward_stack.push_back(NavigationEntry {
3561                    item,
3562                    data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
3563                    timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
3564                    is_preview,
3565                });
3566            }
3567            NavigationMode::GoingForward => {
3568                if state.backward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
3569                    state.backward_stack.pop_front();
3570                }
3571                state.backward_stack.push_back(NavigationEntry {
3572                    item,
3573                    data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
3574                    timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
3575                    is_preview,
3576                });
3577            }
3578            NavigationMode::ClosingItem => {
3579                if state.closed_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
3580                    state.closed_stack.pop_front();
3581                }
3582                state.closed_stack.push_back(NavigationEntry {
3583                    item,
3584                    data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
3585                    timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
3586                    is_preview,
3587                });
3588            }
3589        }
3590        state.did_update(cx);
3591    }
3592
3593    pub fn remove_item(&mut self, item_id: EntityId) {
3594        let mut state = self.0.lock();
3595        state.paths_by_item.remove(&item_id);
3596        state
3597            .backward_stack
3598            .retain(|entry| entry.item.id() != item_id);
3599        state
3600            .forward_stack
3601            .retain(|entry| entry.item.id() != item_id);
3602        state
3603            .closed_stack
3604            .retain(|entry| entry.item.id() != item_id);
3605    }
3606
3607    pub fn path_for_item(&self, item_id: EntityId) -> Option<(ProjectPath, Option<PathBuf>)> {
3608        self.0.lock().paths_by_item.get(&item_id).cloned()
3609    }
3610}
3611
3612impl NavHistoryState {
3613    pub fn did_update(&self, cx: &mut App) {
3614        if let Some(pane) = self.pane.upgrade() {
3615            cx.defer(move |cx| {
3616                pane.update(cx, |pane, cx| pane.history_updated(cx));
3617            });
3618        }
3619    }
3620}
3621
3622fn dirty_message_for(buffer_path: Option<ProjectPath>) -> String {
3623    let path = buffer_path
3624        .as_ref()
3625        .and_then(|p| {
3626            p.path
3627                .to_str()
3628                .and_then(|s| if s.is_empty() { None } else { Some(s) })
3629        })
3630        .unwrap_or("This buffer");
3631    let path = truncate_and_remove_front(path, 80);
3632    format!("{path} contains unsaved edits. Do you want to save it?")
3633}
3634
3635pub fn tab_details(items: &[Box<dyn ItemHandle>], cx: &App) -> Vec<usize> {
3636    let mut tab_details = items.iter().map(|_| 0).collect::<Vec<_>>();
3637    let mut tab_descriptions = HashMap::default();
3638    let mut done = false;
3639    while !done {
3640        done = true;
3641
3642        // Store item indices by their tab description.
3643        for (ix, (item, detail)) in items.iter().zip(&tab_details).enumerate() {
3644            if let Some(description) = item.tab_description(*detail, cx) {
3645                if *detail == 0
3646                    || Some(&description) != item.tab_description(detail - 1, cx).as_ref()
3647                {
3648                    tab_descriptions
3649                        .entry(description)
3650                        .or_insert(Vec::new())
3651                        .push(ix);
3652                }
3653            }
3654        }
3655
3656        // If two or more items have the same tab description, increase their level
3657        // of detail and try again.
3658        for (_, item_ixs) in tab_descriptions.drain() {
3659            if item_ixs.len() > 1 {
3660                done = false;
3661                for ix in item_ixs {
3662                    tab_details[ix] += 1;
3663                }
3664            }
3665        }
3666    }
3667
3668    tab_details
3669}
3670
3671pub fn render_item_indicator(item: Box<dyn ItemHandle>, cx: &App) -> Option<Indicator> {
3672    maybe!({
3673        let indicator_color = match (item.has_conflict(cx), item.is_dirty(cx)) {
3674            (true, _) => Color::Warning,
3675            (_, true) => Color::Accent,
3676            (false, false) => return None,
3677        };
3678
3679        Some(Indicator::dot().color(indicator_color))
3680    })
3681}
3682
3683impl Render for DraggedTab {
3684    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
3685        let ui_font = ThemeSettings::get_global(cx).ui_font.clone();
3686        let label = self.item.tab_content(
3687            TabContentParams {
3688                detail: Some(self.detail),
3689                selected: false,
3690                preview: false,
3691                deemphasized: false,
3692            },
3693            window,
3694            cx,
3695        );
3696        Tab::new("")
3697            .toggle_state(self.is_active)
3698            .child(label)
3699            .render(window, cx)
3700            .font(ui_font)
3701    }
3702}
3703
3704#[cfg(test)]
3705mod tests {
3706    use std::num::NonZero;
3707
3708    use super::*;
3709    use crate::item::test::{TestItem, TestProjectItem};
3710    use gpui::{TestAppContext, VisualTestContext};
3711    use project::FakeFs;
3712    use settings::SettingsStore;
3713    use theme::LoadThemes;
3714
3715    #[gpui::test]
3716    async fn test_remove_active_empty(cx: &mut TestAppContext) {
3717        init_test(cx);
3718        let fs = FakeFs::new(cx.executor());
3719
3720        let project = Project::test(fs, None, cx).await;
3721        let (workspace, cx) =
3722            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
3723        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3724
3725        pane.update_in(cx, |pane, window, cx| {
3726            assert!(
3727                pane.close_active_item(
3728                    &CloseActiveItem {
3729                        save_intent: None,
3730                        close_pinned: false
3731                    },
3732                    window,
3733                    cx
3734                )
3735                .is_none()
3736            )
3737        });
3738    }
3739
3740    #[gpui::test]
3741    async fn test_add_item_capped_to_max_tabs(cx: &mut TestAppContext) {
3742        init_test(cx);
3743        let fs = FakeFs::new(cx.executor());
3744
3745        let project = Project::test(fs, None, cx).await;
3746        let (workspace, cx) =
3747            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
3748        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3749
3750        for i in 0..7 {
3751            add_labeled_item(&pane, format!("{}", i).as_str(), false, cx);
3752        }
3753        set_max_tabs(cx, Some(5));
3754        add_labeled_item(&pane, "7", false, cx);
3755        // Remove items to respect the max tab cap.
3756        assert_item_labels(&pane, ["3", "4", "5", "6", "7*"], cx);
3757        pane.update_in(cx, |pane, window, cx| {
3758            pane.activate_item(0, false, false, window, cx);
3759        });
3760        add_labeled_item(&pane, "X", false, cx);
3761        // Respect activation order.
3762        assert_item_labels(&pane, ["3", "X*", "5", "6", "7"], cx);
3763
3764        for i in 0..7 {
3765            add_labeled_item(&pane, format!("D{}", i).as_str(), true, cx);
3766        }
3767        // Keeps dirty items, even over max tab cap.
3768        assert_item_labels(
3769            &pane,
3770            ["D0^", "D1^", "D2^", "D3^", "D4^", "D5^", "D6*^"],
3771            cx,
3772        );
3773
3774        set_max_tabs(cx, None);
3775        for i in 0..7 {
3776            add_labeled_item(&pane, format!("N{}", i).as_str(), false, cx);
3777        }
3778        // No cap when max tabs is None.
3779        assert_item_labels(
3780            &pane,
3781            [
3782                "D0^", "D1^", "D2^", "D3^", "D4^", "D5^", "D6^", "N0", "N1", "N2", "N3", "N4",
3783                "N5", "N6*",
3784            ],
3785            cx,
3786        );
3787    }
3788
3789    #[gpui::test]
3790    async fn test_add_item_with_new_item(cx: &mut TestAppContext) {
3791        init_test(cx);
3792        let fs = FakeFs::new(cx.executor());
3793
3794        let project = Project::test(fs, None, cx).await;
3795        let (workspace, cx) =
3796            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
3797        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3798
3799        // 1. Add with a destination index
3800        //   a. Add before the active item
3801        set_labeled_items(&pane, ["A", "B*", "C"], cx);
3802        pane.update_in(cx, |pane, window, cx| {
3803            pane.add_item(
3804                Box::new(cx.new(|cx| TestItem::new(cx).with_label("D"))),
3805                false,
3806                false,
3807                Some(0),
3808                window,
3809                cx,
3810            );
3811        });
3812        assert_item_labels(&pane, ["D*", "A", "B", "C"], cx);
3813
3814        //   b. Add after the active item
3815        set_labeled_items(&pane, ["A", "B*", "C"], cx);
3816        pane.update_in(cx, |pane, window, cx| {
3817            pane.add_item(
3818                Box::new(cx.new(|cx| TestItem::new(cx).with_label("D"))),
3819                false,
3820                false,
3821                Some(2),
3822                window,
3823                cx,
3824            );
3825        });
3826        assert_item_labels(&pane, ["A", "B", "D*", "C"], cx);
3827
3828        //   c. Add at the end of the item list (including off the length)
3829        set_labeled_items(&pane, ["A", "B*", "C"], cx);
3830        pane.update_in(cx, |pane, window, cx| {
3831            pane.add_item(
3832                Box::new(cx.new(|cx| TestItem::new(cx).with_label("D"))),
3833                false,
3834                false,
3835                Some(5),
3836                window,
3837                cx,
3838            );
3839        });
3840        assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
3841
3842        // 2. Add without a destination index
3843        //   a. Add with active item at the start of the item list
3844        set_labeled_items(&pane, ["A*", "B", "C"], cx);
3845        pane.update_in(cx, |pane, window, cx| {
3846            pane.add_item(
3847                Box::new(cx.new(|cx| TestItem::new(cx).with_label("D"))),
3848                false,
3849                false,
3850                None,
3851                window,
3852                cx,
3853            );
3854        });
3855        set_labeled_items(&pane, ["A", "D*", "B", "C"], cx);
3856
3857        //   b. Add with active item at the end of the item list
3858        set_labeled_items(&pane, ["A", "B", "C*"], cx);
3859        pane.update_in(cx, |pane, window, cx| {
3860            pane.add_item(
3861                Box::new(cx.new(|cx| TestItem::new(cx).with_label("D"))),
3862                false,
3863                false,
3864                None,
3865                window,
3866                cx,
3867            );
3868        });
3869        assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
3870    }
3871
3872    #[gpui::test]
3873    async fn test_add_item_with_existing_item(cx: &mut TestAppContext) {
3874        init_test(cx);
3875        let fs = FakeFs::new(cx.executor());
3876
3877        let project = Project::test(fs, None, cx).await;
3878        let (workspace, cx) =
3879            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
3880        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3881
3882        // 1. Add with a destination index
3883        //   1a. Add before the active item
3884        let [_, _, _, d] = set_labeled_items(&pane, ["A", "B*", "C", "D"], cx);
3885        pane.update_in(cx, |pane, window, cx| {
3886            pane.add_item(d, false, false, Some(0), window, cx);
3887        });
3888        assert_item_labels(&pane, ["D*", "A", "B", "C"], cx);
3889
3890        //   1b. Add after the active item
3891        let [_, _, _, d] = set_labeled_items(&pane, ["A", "B*", "C", "D"], cx);
3892        pane.update_in(cx, |pane, window, cx| {
3893            pane.add_item(d, false, false, Some(2), window, cx);
3894        });
3895        assert_item_labels(&pane, ["A", "B", "D*", "C"], cx);
3896
3897        //   1c. Add at the end of the item list (including off the length)
3898        let [a, _, _, _] = set_labeled_items(&pane, ["A", "B*", "C", "D"], cx);
3899        pane.update_in(cx, |pane, window, cx| {
3900            pane.add_item(a, false, false, Some(5), window, cx);
3901        });
3902        assert_item_labels(&pane, ["B", "C", "D", "A*"], cx);
3903
3904        //   1d. Add same item to active index
3905        let [_, b, _] = set_labeled_items(&pane, ["A", "B*", "C"], cx);
3906        pane.update_in(cx, |pane, window, cx| {
3907            pane.add_item(b, false, false, Some(1), window, cx);
3908        });
3909        assert_item_labels(&pane, ["A", "B*", "C"], cx);
3910
3911        //   1e. Add item to index after same item in last position
3912        let [_, _, c] = set_labeled_items(&pane, ["A", "B*", "C"], cx);
3913        pane.update_in(cx, |pane, window, cx| {
3914            pane.add_item(c, false, false, Some(2), window, cx);
3915        });
3916        assert_item_labels(&pane, ["A", "B", "C*"], cx);
3917
3918        // 2. Add without a destination index
3919        //   2a. Add with active item at the start of the item list
3920        let [_, _, _, d] = set_labeled_items(&pane, ["A*", "B", "C", "D"], cx);
3921        pane.update_in(cx, |pane, window, cx| {
3922            pane.add_item(d, false, false, None, window, cx);
3923        });
3924        assert_item_labels(&pane, ["A", "D*", "B", "C"], cx);
3925
3926        //   2b. Add with active item at the end of the item list
3927        let [a, _, _, _] = set_labeled_items(&pane, ["A", "B", "C", "D*"], cx);
3928        pane.update_in(cx, |pane, window, cx| {
3929            pane.add_item(a, false, false, None, window, cx);
3930        });
3931        assert_item_labels(&pane, ["B", "C", "D", "A*"], cx);
3932
3933        //   2c. Add active item to active item at end of list
3934        let [_, _, c] = set_labeled_items(&pane, ["A", "B", "C*"], cx);
3935        pane.update_in(cx, |pane, window, cx| {
3936            pane.add_item(c, false, false, None, window, cx);
3937        });
3938        assert_item_labels(&pane, ["A", "B", "C*"], cx);
3939
3940        //   2d. Add active item to active item at start of list
3941        let [a, _, _] = set_labeled_items(&pane, ["A*", "B", "C"], cx);
3942        pane.update_in(cx, |pane, window, cx| {
3943            pane.add_item(a, false, false, None, window, cx);
3944        });
3945        assert_item_labels(&pane, ["A*", "B", "C"], cx);
3946    }
3947
3948    #[gpui::test]
3949    async fn test_add_item_with_same_project_entries(cx: &mut TestAppContext) {
3950        init_test(cx);
3951        let fs = FakeFs::new(cx.executor());
3952
3953        let project = Project::test(fs, None, cx).await;
3954        let (workspace, cx) =
3955            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
3956        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3957
3958        // singleton view
3959        pane.update_in(cx, |pane, window, cx| {
3960            pane.add_item(
3961                Box::new(cx.new(|cx| {
3962                    TestItem::new(cx)
3963                        .with_singleton(true)
3964                        .with_label("buffer 1")
3965                        .with_project_items(&[TestProjectItem::new(1, "one.txt", cx)])
3966                })),
3967                false,
3968                false,
3969                None,
3970                window,
3971                cx,
3972            );
3973        });
3974        assert_item_labels(&pane, ["buffer 1*"], cx);
3975
3976        // new singleton view with the same project entry
3977        pane.update_in(cx, |pane, window, cx| {
3978            pane.add_item(
3979                Box::new(cx.new(|cx| {
3980                    TestItem::new(cx)
3981                        .with_singleton(true)
3982                        .with_label("buffer 1")
3983                        .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
3984                })),
3985                false,
3986                false,
3987                None,
3988                window,
3989                cx,
3990            );
3991        });
3992        assert_item_labels(&pane, ["buffer 1*"], cx);
3993
3994        // new singleton view with different project entry
3995        pane.update_in(cx, |pane, window, cx| {
3996            pane.add_item(
3997                Box::new(cx.new(|cx| {
3998                    TestItem::new(cx)
3999                        .with_singleton(true)
4000                        .with_label("buffer 2")
4001                        .with_project_items(&[TestProjectItem::new(2, "2.txt", cx)])
4002                })),
4003                false,
4004                false,
4005                None,
4006                window,
4007                cx,
4008            );
4009        });
4010        assert_item_labels(&pane, ["buffer 1", "buffer 2*"], cx);
4011
4012        // new multibuffer view with the same project entry
4013        pane.update_in(cx, |pane, window, cx| {
4014            pane.add_item(
4015                Box::new(cx.new(|cx| {
4016                    TestItem::new(cx)
4017                        .with_singleton(false)
4018                        .with_label("multibuffer 1")
4019                        .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
4020                })),
4021                false,
4022                false,
4023                None,
4024                window,
4025                cx,
4026            );
4027        });
4028        assert_item_labels(&pane, ["buffer 1", "buffer 2", "multibuffer 1*"], cx);
4029
4030        // another multibuffer view with the same project entry
4031        pane.update_in(cx, |pane, window, cx| {
4032            pane.add_item(
4033                Box::new(cx.new(|cx| {
4034                    TestItem::new(cx)
4035                        .with_singleton(false)
4036                        .with_label("multibuffer 1b")
4037                        .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
4038                })),
4039                false,
4040                false,
4041                None,
4042                window,
4043                cx,
4044            );
4045        });
4046        assert_item_labels(
4047            &pane,
4048            ["buffer 1", "buffer 2", "multibuffer 1", "multibuffer 1b*"],
4049            cx,
4050        );
4051    }
4052
4053    #[gpui::test]
4054    async fn test_remove_item_ordering_history(cx: &mut TestAppContext) {
4055        init_test(cx);
4056        let fs = FakeFs::new(cx.executor());
4057
4058        let project = Project::test(fs, None, cx).await;
4059        let (workspace, cx) =
4060            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4061        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
4062
4063        add_labeled_item(&pane, "A", false, cx);
4064        add_labeled_item(&pane, "B", false, cx);
4065        add_labeled_item(&pane, "C", false, cx);
4066        add_labeled_item(&pane, "D", false, cx);
4067        assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
4068
4069        pane.update_in(cx, |pane, window, cx| {
4070            pane.activate_item(1, false, false, window, cx)
4071        });
4072        add_labeled_item(&pane, "1", false, cx);
4073        assert_item_labels(&pane, ["A", "B", "1*", "C", "D"], cx);
4074
4075        pane.update_in(cx, |pane, window, cx| {
4076            pane.close_active_item(
4077                &CloseActiveItem {
4078                    save_intent: None,
4079                    close_pinned: false,
4080                },
4081                window,
4082                cx,
4083            )
4084        })
4085        .unwrap()
4086        .await
4087        .unwrap();
4088        assert_item_labels(&pane, ["A", "B*", "C", "D"], cx);
4089
4090        pane.update_in(cx, |pane, window, cx| {
4091            pane.activate_item(3, false, false, window, cx)
4092        });
4093        assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
4094
4095        pane.update_in(cx, |pane, window, cx| {
4096            pane.close_active_item(
4097                &CloseActiveItem {
4098                    save_intent: None,
4099                    close_pinned: false,
4100                },
4101                window,
4102                cx,
4103            )
4104        })
4105        .unwrap()
4106        .await
4107        .unwrap();
4108        assert_item_labels(&pane, ["A", "B*", "C"], cx);
4109
4110        pane.update_in(cx, |pane, window, cx| {
4111            pane.close_active_item(
4112                &CloseActiveItem {
4113                    save_intent: None,
4114                    close_pinned: false,
4115                },
4116                window,
4117                cx,
4118            )
4119        })
4120        .unwrap()
4121        .await
4122        .unwrap();
4123        assert_item_labels(&pane, ["A", "C*"], cx);
4124
4125        pane.update_in(cx, |pane, window, cx| {
4126            pane.close_active_item(
4127                &CloseActiveItem {
4128                    save_intent: None,
4129                    close_pinned: false,
4130                },
4131                window,
4132                cx,
4133            )
4134        })
4135        .unwrap()
4136        .await
4137        .unwrap();
4138        assert_item_labels(&pane, ["A*"], cx);
4139    }
4140
4141    #[gpui::test]
4142    async fn test_remove_item_ordering_neighbour(cx: &mut TestAppContext) {
4143        init_test(cx);
4144        cx.update_global::<SettingsStore, ()>(|s, cx| {
4145            s.update_user_settings::<ItemSettings>(cx, |s| {
4146                s.activate_on_close = Some(ActivateOnClose::Neighbour);
4147            });
4148        });
4149        let fs = FakeFs::new(cx.executor());
4150
4151        let project = Project::test(fs, None, cx).await;
4152        let (workspace, cx) =
4153            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4154        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
4155
4156        add_labeled_item(&pane, "A", false, cx);
4157        add_labeled_item(&pane, "B", false, cx);
4158        add_labeled_item(&pane, "C", false, cx);
4159        add_labeled_item(&pane, "D", false, cx);
4160        assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
4161
4162        pane.update_in(cx, |pane, window, cx| {
4163            pane.activate_item(1, false, false, window, cx)
4164        });
4165        add_labeled_item(&pane, "1", false, cx);
4166        assert_item_labels(&pane, ["A", "B", "1*", "C", "D"], cx);
4167
4168        pane.update_in(cx, |pane, window, cx| {
4169            pane.close_active_item(
4170                &CloseActiveItem {
4171                    save_intent: None,
4172                    close_pinned: false,
4173                },
4174                window,
4175                cx,
4176            )
4177        })
4178        .unwrap()
4179        .await
4180        .unwrap();
4181        assert_item_labels(&pane, ["A", "B", "C*", "D"], cx);
4182
4183        pane.update_in(cx, |pane, window, cx| {
4184            pane.activate_item(3, false, false, window, cx)
4185        });
4186        assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
4187
4188        pane.update_in(cx, |pane, window, cx| {
4189            pane.close_active_item(
4190                &CloseActiveItem {
4191                    save_intent: None,
4192                    close_pinned: false,
4193                },
4194                window,
4195                cx,
4196            )
4197        })
4198        .unwrap()
4199        .await
4200        .unwrap();
4201        assert_item_labels(&pane, ["A", "B", "C*"], cx);
4202
4203        pane.update_in(cx, |pane, window, cx| {
4204            pane.close_active_item(
4205                &CloseActiveItem {
4206                    save_intent: None,
4207                    close_pinned: false,
4208                },
4209                window,
4210                cx,
4211            )
4212        })
4213        .unwrap()
4214        .await
4215        .unwrap();
4216        assert_item_labels(&pane, ["A", "B*"], cx);
4217
4218        pane.update_in(cx, |pane, window, cx| {
4219            pane.close_active_item(
4220                &CloseActiveItem {
4221                    save_intent: None,
4222                    close_pinned: false,
4223                },
4224                window,
4225                cx,
4226            )
4227        })
4228        .unwrap()
4229        .await
4230        .unwrap();
4231        assert_item_labels(&pane, ["A*"], cx);
4232    }
4233
4234    #[gpui::test]
4235    async fn test_remove_item_ordering_left_neighbour(cx: &mut TestAppContext) {
4236        init_test(cx);
4237        cx.update_global::<SettingsStore, ()>(|s, cx| {
4238            s.update_user_settings::<ItemSettings>(cx, |s| {
4239                s.activate_on_close = Some(ActivateOnClose::LeftNeighbour);
4240            });
4241        });
4242        let fs = FakeFs::new(cx.executor());
4243
4244        let project = Project::test(fs, None, cx).await;
4245        let (workspace, cx) =
4246            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4247        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
4248
4249        add_labeled_item(&pane, "A", false, cx);
4250        add_labeled_item(&pane, "B", false, cx);
4251        add_labeled_item(&pane, "C", false, cx);
4252        add_labeled_item(&pane, "D", false, cx);
4253        assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
4254
4255        pane.update_in(cx, |pane, window, cx| {
4256            pane.activate_item(1, false, false, window, cx)
4257        });
4258        add_labeled_item(&pane, "1", false, cx);
4259        assert_item_labels(&pane, ["A", "B", "1*", "C", "D"], cx);
4260
4261        pane.update_in(cx, |pane, window, cx| {
4262            pane.close_active_item(
4263                &CloseActiveItem {
4264                    save_intent: None,
4265                    close_pinned: false,
4266                },
4267                window,
4268                cx,
4269            )
4270        })
4271        .unwrap()
4272        .await
4273        .unwrap();
4274        assert_item_labels(&pane, ["A", "B*", "C", "D"], cx);
4275
4276        pane.update_in(cx, |pane, window, cx| {
4277            pane.activate_item(3, false, false, window, cx)
4278        });
4279        assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
4280
4281        pane.update_in(cx, |pane, window, cx| {
4282            pane.close_active_item(
4283                &CloseActiveItem {
4284                    save_intent: None,
4285                    close_pinned: false,
4286                },
4287                window,
4288                cx,
4289            )
4290        })
4291        .unwrap()
4292        .await
4293        .unwrap();
4294        assert_item_labels(&pane, ["A", "B", "C*"], cx);
4295
4296        pane.update_in(cx, |pane, window, cx| {
4297            pane.activate_item(0, false, false, window, cx)
4298        });
4299        assert_item_labels(&pane, ["A*", "B", "C"], cx);
4300
4301        pane.update_in(cx, |pane, window, cx| {
4302            pane.close_active_item(
4303                &CloseActiveItem {
4304                    save_intent: None,
4305                    close_pinned: false,
4306                },
4307                window,
4308                cx,
4309            )
4310        })
4311        .unwrap()
4312        .await
4313        .unwrap();
4314        assert_item_labels(&pane, ["B*", "C"], cx);
4315
4316        pane.update_in(cx, |pane, window, cx| {
4317            pane.close_active_item(
4318                &CloseActiveItem {
4319                    save_intent: None,
4320                    close_pinned: false,
4321                },
4322                window,
4323                cx,
4324            )
4325        })
4326        .unwrap()
4327        .await
4328        .unwrap();
4329        assert_item_labels(&pane, ["C*"], cx);
4330    }
4331
4332    #[gpui::test]
4333    async fn test_close_inactive_items(cx: &mut TestAppContext) {
4334        init_test(cx);
4335        let fs = FakeFs::new(cx.executor());
4336
4337        let project = Project::test(fs, None, cx).await;
4338        let (workspace, cx) =
4339            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4340        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
4341
4342        set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx);
4343
4344        pane.update_in(cx, |pane, window, cx| {
4345            pane.close_inactive_items(
4346                &CloseInactiveItems {
4347                    save_intent: None,
4348                    close_pinned: false,
4349                },
4350                window,
4351                cx,
4352            )
4353        })
4354        .unwrap()
4355        .await
4356        .unwrap();
4357        assert_item_labels(&pane, ["C*"], cx);
4358    }
4359
4360    #[gpui::test]
4361    async fn test_close_clean_items(cx: &mut TestAppContext) {
4362        init_test(cx);
4363        let fs = FakeFs::new(cx.executor());
4364
4365        let project = Project::test(fs, None, cx).await;
4366        let (workspace, cx) =
4367            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4368        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
4369
4370        add_labeled_item(&pane, "A", true, cx);
4371        add_labeled_item(&pane, "B", false, cx);
4372        add_labeled_item(&pane, "C", true, cx);
4373        add_labeled_item(&pane, "D", false, cx);
4374        add_labeled_item(&pane, "E", false, cx);
4375        assert_item_labels(&pane, ["A^", "B", "C^", "D", "E*"], cx);
4376
4377        pane.update_in(cx, |pane, window, cx| {
4378            pane.close_clean_items(
4379                &CloseCleanItems {
4380                    close_pinned: false,
4381                },
4382                window,
4383                cx,
4384            )
4385        })
4386        .unwrap()
4387        .await
4388        .unwrap();
4389        assert_item_labels(&pane, ["A^", "C*^"], cx);
4390    }
4391
4392    #[gpui::test]
4393    async fn test_close_items_to_the_left(cx: &mut TestAppContext) {
4394        init_test(cx);
4395        let fs = FakeFs::new(cx.executor());
4396
4397        let project = Project::test(fs, None, cx).await;
4398        let (workspace, cx) =
4399            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4400        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
4401
4402        set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx);
4403
4404        pane.update_in(cx, |pane, window, cx| {
4405            pane.close_items_to_the_left(
4406                &CloseItemsToTheLeft {
4407                    close_pinned: false,
4408                },
4409                window,
4410                cx,
4411            )
4412        })
4413        .unwrap()
4414        .await
4415        .unwrap();
4416        assert_item_labels(&pane, ["C*", "D", "E"], cx);
4417    }
4418
4419    #[gpui::test]
4420    async fn test_close_items_to_the_right(cx: &mut TestAppContext) {
4421        init_test(cx);
4422        let fs = FakeFs::new(cx.executor());
4423
4424        let project = Project::test(fs, None, cx).await;
4425        let (workspace, cx) =
4426            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4427        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
4428
4429        set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx);
4430
4431        pane.update_in(cx, |pane, window, cx| {
4432            pane.close_items_to_the_right(
4433                &CloseItemsToTheRight {
4434                    close_pinned: false,
4435                },
4436                window,
4437                cx,
4438            )
4439        })
4440        .unwrap()
4441        .await
4442        .unwrap();
4443        assert_item_labels(&pane, ["A", "B", "C*"], cx);
4444    }
4445
4446    #[gpui::test]
4447    async fn test_close_all_items(cx: &mut TestAppContext) {
4448        init_test(cx);
4449        let fs = FakeFs::new(cx.executor());
4450
4451        let project = Project::test(fs, None, cx).await;
4452        let (workspace, cx) =
4453            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4454        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
4455
4456        let item_a = add_labeled_item(&pane, "A", false, cx);
4457        add_labeled_item(&pane, "B", false, cx);
4458        add_labeled_item(&pane, "C", false, cx);
4459        assert_item_labels(&pane, ["A", "B", "C*"], cx);
4460
4461        pane.update_in(cx, |pane, window, cx| {
4462            let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4463            pane.pin_tab_at(ix, window, cx);
4464            pane.close_all_items(
4465                &CloseAllItems {
4466                    save_intent: None,
4467                    close_pinned: false,
4468                },
4469                window,
4470                cx,
4471            )
4472        })
4473        .unwrap()
4474        .await
4475        .unwrap();
4476        assert_item_labels(&pane, ["A*"], cx);
4477
4478        pane.update_in(cx, |pane, window, cx| {
4479            let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4480            pane.unpin_tab_at(ix, window, cx);
4481            pane.close_all_items(
4482                &CloseAllItems {
4483                    save_intent: None,
4484                    close_pinned: false,
4485                },
4486                window,
4487                cx,
4488            )
4489        })
4490        .unwrap()
4491        .await
4492        .unwrap();
4493
4494        assert_item_labels(&pane, [], cx);
4495
4496        add_labeled_item(&pane, "A", true, cx).update(cx, |item, cx| {
4497            item.project_items
4498                .push(TestProjectItem::new_dirty(1, "A.txt", cx))
4499        });
4500        add_labeled_item(&pane, "B", true, cx).update(cx, |item, cx| {
4501            item.project_items
4502                .push(TestProjectItem::new_dirty(2, "B.txt", cx))
4503        });
4504        add_labeled_item(&pane, "C", true, cx).update(cx, |item, cx| {
4505            item.project_items
4506                .push(TestProjectItem::new_dirty(3, "C.txt", cx))
4507        });
4508        assert_item_labels(&pane, ["A^", "B^", "C*^"], cx);
4509
4510        let save = pane
4511            .update_in(cx, |pane, window, cx| {
4512                pane.close_all_items(
4513                    &CloseAllItems {
4514                        save_intent: None,
4515                        close_pinned: false,
4516                    },
4517                    window,
4518                    cx,
4519                )
4520            })
4521            .unwrap();
4522
4523        cx.executor().run_until_parked();
4524        cx.simulate_prompt_answer("Save all");
4525        save.await.unwrap();
4526        assert_item_labels(&pane, [], cx);
4527
4528        add_labeled_item(&pane, "A", true, cx);
4529        add_labeled_item(&pane, "B", true, cx);
4530        add_labeled_item(&pane, "C", true, cx);
4531        assert_item_labels(&pane, ["A^", "B^", "C*^"], cx);
4532        let save = pane
4533            .update_in(cx, |pane, window, cx| {
4534                pane.close_all_items(
4535                    &CloseAllItems {
4536                        save_intent: None,
4537                        close_pinned: false,
4538                    },
4539                    window,
4540                    cx,
4541                )
4542            })
4543            .unwrap();
4544
4545        cx.executor().run_until_parked();
4546        cx.simulate_prompt_answer("Discard all");
4547        save.await.unwrap();
4548        assert_item_labels(&pane, [], cx);
4549    }
4550
4551    #[gpui::test]
4552    async fn test_close_with_save_intent(cx: &mut TestAppContext) {
4553        init_test(cx);
4554        let fs = FakeFs::new(cx.executor());
4555
4556        let project = Project::test(fs, None, cx).await;
4557        let (workspace, cx) =
4558            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
4559        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
4560
4561        let a = cx.update(|_, cx| TestProjectItem::new_dirty(1, "A.txt", cx));
4562        let b = cx.update(|_, cx| TestProjectItem::new_dirty(1, "B.txt", cx));
4563        let c = cx.update(|_, cx| TestProjectItem::new_dirty(1, "C.txt", cx));
4564
4565        add_labeled_item(&pane, "AB", true, cx).update(cx, |item, _| {
4566            item.project_items.push(a.clone());
4567            item.project_items.push(b.clone());
4568        });
4569        add_labeled_item(&pane, "C", true, cx)
4570            .update(cx, |item, _| item.project_items.push(c.clone()));
4571        assert_item_labels(&pane, ["AB^", "C*^"], cx);
4572
4573        pane.update_in(cx, |pane, window, cx| {
4574            pane.close_all_items(
4575                &CloseAllItems {
4576                    save_intent: Some(SaveIntent::Save),
4577                    close_pinned: false,
4578                },
4579                window,
4580                cx,
4581            )
4582        })
4583        .unwrap()
4584        .await
4585        .unwrap();
4586
4587        assert_item_labels(&pane, [], cx);
4588        cx.update(|_, cx| {
4589            assert!(!a.read(cx).is_dirty);
4590            assert!(!b.read(cx).is_dirty);
4591            assert!(!c.read(cx).is_dirty);
4592        });
4593    }
4594
4595    #[gpui::test]
4596    async fn test_close_all_items_including_pinned(cx: &mut TestAppContext) {
4597        init_test(cx);
4598        let fs = FakeFs::new(cx.executor());
4599
4600        let project = Project::test(fs, None, cx).await;
4601        let (workspace, cx) =
4602            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
4603        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
4604
4605        let item_a = add_labeled_item(&pane, "A", false, cx);
4606        add_labeled_item(&pane, "B", false, cx);
4607        add_labeled_item(&pane, "C", false, cx);
4608        assert_item_labels(&pane, ["A", "B", "C*"], cx);
4609
4610        pane.update_in(cx, |pane, window, cx| {
4611            let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4612            pane.pin_tab_at(ix, window, cx);
4613            pane.close_all_items(
4614                &CloseAllItems {
4615                    save_intent: None,
4616                    close_pinned: true,
4617                },
4618                window,
4619                cx,
4620            )
4621        })
4622        .unwrap()
4623        .await
4624        .unwrap();
4625        assert_item_labels(&pane, [], cx);
4626    }
4627
4628    #[gpui::test]
4629    async fn test_close_pinned_tab_with_non_pinned_in_same_pane(cx: &mut TestAppContext) {
4630        init_test(cx);
4631        let fs = FakeFs::new(cx.executor());
4632        let project = Project::test(fs, None, cx).await;
4633        let (workspace, cx) =
4634            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
4635
4636        // Non-pinned tabs in same pane
4637        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
4638        add_labeled_item(&pane, "A", false, cx);
4639        add_labeled_item(&pane, "B", false, cx);
4640        add_labeled_item(&pane, "C", false, cx);
4641        pane.update_in(cx, |pane, window, cx| {
4642            pane.pin_tab_at(0, window, cx);
4643        });
4644        set_labeled_items(&pane, ["A*", "B", "C"], cx);
4645        pane.update_in(cx, |pane, window, cx| {
4646            pane.close_active_item(
4647                &CloseActiveItem {
4648                    save_intent: None,
4649                    close_pinned: false,
4650                },
4651                window,
4652                cx,
4653            );
4654        });
4655        // Non-pinned tab should be active
4656        assert_item_labels(&pane, ["A", "B*", "C"], cx);
4657    }
4658
4659    #[gpui::test]
4660    async fn test_close_pinned_tab_with_non_pinned_in_different_pane(cx: &mut TestAppContext) {
4661        init_test(cx);
4662        let fs = FakeFs::new(cx.executor());
4663        let project = Project::test(fs, None, cx).await;
4664        let (workspace, cx) =
4665            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
4666
4667        // No non-pinned tabs in same pane, non-pinned tabs in another pane
4668        let pane1 = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
4669        let pane2 = workspace.update_in(cx, |workspace, window, cx| {
4670            workspace.split_pane(pane1.clone(), SplitDirection::Right, window, cx)
4671        });
4672        add_labeled_item(&pane1, "A", false, cx);
4673        pane1.update_in(cx, |pane, window, cx| {
4674            pane.pin_tab_at(0, window, cx);
4675        });
4676        set_labeled_items(&pane1, ["A*"], cx);
4677        add_labeled_item(&pane2, "B", false, cx);
4678        set_labeled_items(&pane2, ["B"], cx);
4679        pane1.update_in(cx, |pane, window, cx| {
4680            pane.close_active_item(
4681                &CloseActiveItem {
4682                    save_intent: None,
4683                    close_pinned: false,
4684                },
4685                window,
4686                cx,
4687            );
4688        });
4689        //  Non-pinned tab of other pane should be active
4690        assert_item_labels(&pane2, ["B*"], cx);
4691    }
4692
4693    fn init_test(cx: &mut TestAppContext) {
4694        cx.update(|cx| {
4695            let settings_store = SettingsStore::test(cx);
4696            cx.set_global(settings_store);
4697            theme::init(LoadThemes::JustBase, cx);
4698            crate::init_settings(cx);
4699            Project::init_settings(cx);
4700        });
4701    }
4702
4703    fn set_max_tabs(cx: &mut TestAppContext, value: Option<usize>) {
4704        cx.update_global(|store: &mut SettingsStore, cx| {
4705            store.update_user_settings::<WorkspaceSettings>(cx, |settings| {
4706                settings.max_tabs = value.map(|v| NonZero::new(v).unwrap())
4707            });
4708        });
4709    }
4710
4711    fn add_labeled_item(
4712        pane: &Entity<Pane>,
4713        label: &str,
4714        is_dirty: bool,
4715        cx: &mut VisualTestContext,
4716    ) -> Box<Entity<TestItem>> {
4717        pane.update_in(cx, |pane, window, cx| {
4718            let labeled_item =
4719                Box::new(cx.new(|cx| TestItem::new(cx).with_label(label).with_dirty(is_dirty)));
4720            pane.add_item(labeled_item.clone(), false, false, None, window, cx);
4721            labeled_item
4722        })
4723    }
4724
4725    fn set_labeled_items<const COUNT: usize>(
4726        pane: &Entity<Pane>,
4727        labels: [&str; COUNT],
4728        cx: &mut VisualTestContext,
4729    ) -> [Box<Entity<TestItem>>; COUNT] {
4730        pane.update_in(cx, |pane, window, cx| {
4731            pane.items.clear();
4732            let mut active_item_index = 0;
4733
4734            let mut index = 0;
4735            let items = labels.map(|mut label| {
4736                if label.ends_with('*') {
4737                    label = label.trim_end_matches('*');
4738                    active_item_index = index;
4739                }
4740
4741                let labeled_item = Box::new(cx.new(|cx| TestItem::new(cx).with_label(label)));
4742                pane.add_item(labeled_item.clone(), false, false, None, window, cx);
4743                index += 1;
4744                labeled_item
4745            });
4746
4747            pane.activate_item(active_item_index, false, false, window, cx);
4748
4749            items
4750        })
4751    }
4752
4753    // Assert the item label, with the active item label suffixed with a '*'
4754    #[track_caller]
4755    fn assert_item_labels<const COUNT: usize>(
4756        pane: &Entity<Pane>,
4757        expected_states: [&str; COUNT],
4758        cx: &mut VisualTestContext,
4759    ) {
4760        let actual_states = pane.update(cx, |pane, cx| {
4761            pane.items
4762                .iter()
4763                .enumerate()
4764                .map(|(ix, item)| {
4765                    let mut state = item
4766                        .to_any()
4767                        .downcast::<TestItem>()
4768                        .unwrap()
4769                        .read(cx)
4770                        .label
4771                        .clone();
4772                    if ix == pane.active_item_index {
4773                        state.push('*');
4774                    }
4775                    if item.is_dirty(cx) {
4776                        state.push('^');
4777                    }
4778                    state
4779                })
4780                .collect::<Vec<_>>()
4781        });
4782        assert_eq!(
4783            actual_states, expected_states,
4784            "pane items do not match expectation"
4785        );
4786    }
4787}