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            },
2151            window,
2152            cx,
2153        );
2154
2155        let item_diagnostic = item
2156            .project_path(cx)
2157            .map_or(None, |project_path| self.diagnostics.get(&project_path));
2158
2159        let decorated_icon = item_diagnostic.map_or(None, |diagnostic| {
2160            let icon = match item.tab_icon(window, cx) {
2161                Some(icon) => icon,
2162                None => return None,
2163            };
2164
2165            let knockout_item_color = if is_active {
2166                cx.theme().colors().tab_active_background
2167            } else {
2168                cx.theme().colors().tab_bar_background
2169            };
2170
2171            let (icon_decoration, icon_color) = if matches!(diagnostic, &DiagnosticSeverity::ERROR)
2172            {
2173                (IconDecorationKind::X, Color::Error)
2174            } else {
2175                (IconDecorationKind::Triangle, Color::Warning)
2176            };
2177
2178            Some(DecoratedIcon::new(
2179                icon.size(IconSize::Small).color(Color::Muted),
2180                Some(
2181                    IconDecoration::new(icon_decoration, knockout_item_color, cx)
2182                        .color(icon_color.color(cx))
2183                        .position(Point {
2184                            x: px(-2.),
2185                            y: px(-2.),
2186                        }),
2187                ),
2188            ))
2189        });
2190
2191        let icon = if decorated_icon.is_none() {
2192            match item_diagnostic {
2193                Some(&DiagnosticSeverity::ERROR) => None,
2194                Some(&DiagnosticSeverity::WARNING) => None,
2195                _ => item
2196                    .tab_icon(window, cx)
2197                    .map(|icon| icon.color(Color::Muted)),
2198            }
2199            .map(|icon| icon.size(IconSize::Small))
2200        } else {
2201            None
2202        };
2203
2204        let settings = ItemSettings::get_global(cx);
2205        let close_side = &settings.close_position;
2206        let show_close_button = &settings.show_close_button;
2207        let indicator = render_item_indicator(item.boxed_clone(), cx);
2208        let item_id = item.item_id();
2209        let is_first_item = ix == 0;
2210        let is_last_item = ix == self.items.len() - 1;
2211        let is_pinned = self.is_tab_pinned(ix);
2212        let position_relative_to_active_item = ix.cmp(&self.active_item_index);
2213
2214        let tab = Tab::new(ix)
2215            .position(if is_first_item {
2216                TabPosition::First
2217            } else if is_last_item {
2218                TabPosition::Last
2219            } else {
2220                TabPosition::Middle(position_relative_to_active_item)
2221            })
2222            .close_side(match close_side {
2223                ClosePosition::Left => ui::TabCloseSide::Start,
2224                ClosePosition::Right => ui::TabCloseSide::End,
2225            })
2226            .toggle_state(is_active)
2227            .on_click(cx.listener(move |pane: &mut Self, _, window, cx| {
2228                pane.activate_item(ix, true, true, window, cx)
2229            }))
2230            // TODO: This should be a click listener with the middle mouse button instead of a mouse down listener.
2231            .on_mouse_down(
2232                MouseButton::Middle,
2233                cx.listener(move |pane, _event, window, cx| {
2234                    pane.close_item_by_id(item_id, SaveIntent::Close, window, cx)
2235                        .detach_and_log_err(cx);
2236                }),
2237            )
2238            .on_mouse_down(
2239                MouseButton::Left,
2240                cx.listener(move |pane, event: &MouseDownEvent, _, cx| {
2241                    if let Some(id) = pane.preview_item_id {
2242                        if id == item_id && event.click_count > 1 {
2243                            pane.set_preview_item_id(None, cx);
2244                        }
2245                    }
2246                }),
2247            )
2248            .on_drag(
2249                DraggedTab {
2250                    item: item.boxed_clone(),
2251                    pane: cx.entity().clone(),
2252                    detail,
2253                    is_active,
2254                    ix,
2255                },
2256                |tab, _, _, cx| cx.new(|_| tab.clone()),
2257            )
2258            .drag_over::<DraggedTab>(|tab, _, _, cx| {
2259                tab.bg(cx.theme().colors().drop_target_background)
2260            })
2261            .drag_over::<DraggedSelection>(|tab, _, _, cx| {
2262                tab.bg(cx.theme().colors().drop_target_background)
2263            })
2264            .when_some(self.can_drop_predicate.clone(), |this, p| {
2265                this.can_drop(move |a, window, cx| p(a, window, cx))
2266            })
2267            .on_drop(
2268                cx.listener(move |this, dragged_tab: &DraggedTab, window, cx| {
2269                    this.drag_split_direction = None;
2270                    this.handle_tab_drop(dragged_tab, ix, window, cx)
2271                }),
2272            )
2273            .on_drop(
2274                cx.listener(move |this, selection: &DraggedSelection, window, cx| {
2275                    this.drag_split_direction = None;
2276                    this.handle_dragged_selection_drop(selection, Some(ix), window, cx)
2277                }),
2278            )
2279            .on_drop(cx.listener(move |this, paths, window, cx| {
2280                this.drag_split_direction = None;
2281                this.handle_external_paths_drop(paths, window, cx)
2282            }))
2283            .when_some(item.tab_tooltip_content(cx), |tab, content| match content {
2284                TabTooltipContent::Text(text) => tab.tooltip(Tooltip::text(text.clone())),
2285                TabTooltipContent::Custom(element_fn) => {
2286                    tab.tooltip(move |window, cx| element_fn(window, cx))
2287                }
2288            })
2289            .start_slot::<Indicator>(indicator)
2290            .map(|this| {
2291                let end_slot_action: &'static dyn Action;
2292                let end_slot_tooltip_text: &'static str;
2293                let end_slot = if is_pinned {
2294                    end_slot_action = &TogglePinTab;
2295                    end_slot_tooltip_text = "Unpin Tab";
2296                    IconButton::new("unpin tab", IconName::Pin)
2297                        .shape(IconButtonShape::Square)
2298                        .icon_color(Color::Muted)
2299                        .size(ButtonSize::None)
2300                        .icon_size(IconSize::XSmall)
2301                        .on_click(cx.listener(move |pane, _, window, cx| {
2302                            pane.unpin_tab_at(ix, window, cx);
2303                        }))
2304                } else {
2305                    end_slot_action = &CloseActiveItem {
2306                        save_intent: None,
2307                        close_pinned: false,
2308                    };
2309                    end_slot_tooltip_text = "Close Tab";
2310                    match show_close_button {
2311                        ShowCloseButton::Always => IconButton::new("close tab", IconName::Close),
2312                        ShowCloseButton::Hover => {
2313                            IconButton::new("close tab", IconName::Close).visible_on_hover("")
2314                        }
2315                        ShowCloseButton::Hidden => return this,
2316                    }
2317                    .shape(IconButtonShape::Square)
2318                    .icon_color(Color::Muted)
2319                    .size(ButtonSize::None)
2320                    .icon_size(IconSize::XSmall)
2321                    .on_click(cx.listener(move |pane, _, window, cx| {
2322                        pane.close_item_by_id(item_id, SaveIntent::Close, window, cx)
2323                            .detach_and_log_err(cx);
2324                    }))
2325                }
2326                .map(|this| {
2327                    if is_active {
2328                        let focus_handle = focus_handle.clone();
2329                        this.tooltip(move |window, cx| {
2330                            Tooltip::for_action_in(
2331                                end_slot_tooltip_text,
2332                                end_slot_action,
2333                                &focus_handle,
2334                                window,
2335                                cx,
2336                            )
2337                        })
2338                    } else {
2339                        this.tooltip(Tooltip::text(end_slot_tooltip_text))
2340                    }
2341                });
2342                this.end_slot(end_slot)
2343            })
2344            .child(
2345                h_flex()
2346                    .gap_1()
2347                    .items_center()
2348                    .children(
2349                        std::iter::once(if let Some(decorated_icon) = decorated_icon {
2350                            Some(div().child(decorated_icon.into_any_element()))
2351                        } else if let Some(icon) = icon {
2352                            Some(div().child(icon.into_any_element()))
2353                        } else {
2354                            None
2355                        })
2356                        .flatten(),
2357                    )
2358                    .child(label),
2359            );
2360
2361        let single_entry_to_resolve = self.items[ix]
2362            .is_singleton(cx)
2363            .then(|| self.items[ix].project_entry_ids(cx).get(0).copied())
2364            .flatten();
2365
2366        let total_items = self.items.len();
2367        let has_items_to_left = ix > 0;
2368        let has_items_to_right = ix < total_items - 1;
2369        let is_pinned = self.is_tab_pinned(ix);
2370        let pane = cx.entity().downgrade();
2371        let menu_context = item.item_focus_handle(cx);
2372        right_click_menu(ix).trigger(tab).menu(move |window, cx| {
2373            let pane = pane.clone();
2374            let menu_context = menu_context.clone();
2375            ContextMenu::build(window, cx, move |mut menu, window, cx| {
2376                if let Some(pane) = pane.upgrade() {
2377                    menu = menu
2378                        .entry(
2379                            "Close",
2380                            Some(Box::new(CloseActiveItem {
2381                                save_intent: None,
2382                                close_pinned: true,
2383                            })),
2384                            window.handler_for(&pane, move |pane, window, cx| {
2385                                pane.close_item_by_id(item_id, SaveIntent::Close, window, cx)
2386                                    .detach_and_log_err(cx);
2387                            }),
2388                        )
2389                        .item(ContextMenuItem::Entry(
2390                            ContextMenuEntry::new("Close Others")
2391                                .action(Box::new(CloseInactiveItems {
2392                                    save_intent: None,
2393                                    close_pinned: false,
2394                                }))
2395                                .disabled(total_items == 1)
2396                                .handler(window.handler_for(&pane, move |pane, window, cx| {
2397                                    pane.close_items(window, cx, SaveIntent::Close, |id| {
2398                                        id != item_id
2399                                    })
2400                                    .detach_and_log_err(cx);
2401                                })),
2402                        ))
2403                        .separator()
2404                        .item(ContextMenuItem::Entry(
2405                            ContextMenuEntry::new("Close Left")
2406                                .action(Box::new(CloseItemsToTheLeft {
2407                                    close_pinned: false,
2408                                }))
2409                                .disabled(!has_items_to_left)
2410                                .handler(window.handler_for(&pane, move |pane, window, cx| {
2411                                    pane.close_items_to_the_left_by_id(
2412                                        item_id,
2413                                        &CloseItemsToTheLeft {
2414                                            close_pinned: false,
2415                                        },
2416                                        pane.get_non_closeable_item_ids(false),
2417                                        window,
2418                                        cx,
2419                                    )
2420                                    .detach_and_log_err(cx);
2421                                })),
2422                        ))
2423                        .item(ContextMenuItem::Entry(
2424                            ContextMenuEntry::new("Close Right")
2425                                .action(Box::new(CloseItemsToTheRight {
2426                                    close_pinned: false,
2427                                }))
2428                                .disabled(!has_items_to_right)
2429                                .handler(window.handler_for(&pane, move |pane, window, cx| {
2430                                    pane.close_items_to_the_right_by_id(
2431                                        item_id,
2432                                        &CloseItemsToTheRight {
2433                                            close_pinned: false,
2434                                        },
2435                                        pane.get_non_closeable_item_ids(false),
2436                                        window,
2437                                        cx,
2438                                    )
2439                                    .detach_and_log_err(cx);
2440                                })),
2441                        ))
2442                        .separator()
2443                        .entry(
2444                            "Close Clean",
2445                            Some(Box::new(CloseCleanItems {
2446                                close_pinned: false,
2447                            })),
2448                            window.handler_for(&pane, move |pane, window, cx| {
2449                                if let Some(task) = pane.close_clean_items(
2450                                    &CloseCleanItems {
2451                                        close_pinned: false,
2452                                    },
2453                                    window,
2454                                    cx,
2455                                ) {
2456                                    task.detach_and_log_err(cx)
2457                                }
2458                            }),
2459                        )
2460                        .entry(
2461                            "Close All",
2462                            Some(Box::new(CloseAllItems {
2463                                save_intent: None,
2464                                close_pinned: false,
2465                            })),
2466                            window.handler_for(&pane, |pane, window, cx| {
2467                                if let Some(task) = pane.close_all_items(
2468                                    &CloseAllItems {
2469                                        save_intent: None,
2470                                        close_pinned: false,
2471                                    },
2472                                    window,
2473                                    cx,
2474                                ) {
2475                                    task.detach_and_log_err(cx)
2476                                }
2477                            }),
2478                        );
2479
2480                    let pin_tab_entries = |menu: ContextMenu| {
2481                        menu.separator().map(|this| {
2482                            if is_pinned {
2483                                this.entry(
2484                                    "Unpin Tab",
2485                                    Some(TogglePinTab.boxed_clone()),
2486                                    window.handler_for(&pane, move |pane, window, cx| {
2487                                        pane.unpin_tab_at(ix, window, cx);
2488                                    }),
2489                                )
2490                            } else {
2491                                this.entry(
2492                                    "Pin Tab",
2493                                    Some(TogglePinTab.boxed_clone()),
2494                                    window.handler_for(&pane, move |pane, window, cx| {
2495                                        pane.pin_tab_at(ix, window, cx);
2496                                    }),
2497                                )
2498                            }
2499                        })
2500                    };
2501                    if let Some(entry) = single_entry_to_resolve {
2502                        let project_path = pane
2503                            .read(cx)
2504                            .item_for_entry(entry, cx)
2505                            .and_then(|item| item.project_path(cx));
2506                        let worktree = project_path.as_ref().and_then(|project_path| {
2507                            pane.read(cx)
2508                                .project
2509                                .upgrade()?
2510                                .read(cx)
2511                                .worktree_for_id(project_path.worktree_id, cx)
2512                        });
2513                        let has_relative_path = worktree.as_ref().is_some_and(|worktree| {
2514                            worktree
2515                                .read(cx)
2516                                .root_entry()
2517                                .map_or(false, |entry| entry.is_dir())
2518                        });
2519
2520                        let entry_abs_path = pane.read(cx).entry_abs_path(entry, cx);
2521                        let parent_abs_path = entry_abs_path
2522                            .as_deref()
2523                            .and_then(|abs_path| Some(abs_path.parent()?.to_path_buf()));
2524                        let relative_path = project_path
2525                            .map(|project_path| project_path.path)
2526                            .filter(|_| has_relative_path);
2527
2528                        let visible_in_project_panel = relative_path.is_some()
2529                            && worktree.is_some_and(|worktree| worktree.read(cx).is_visible());
2530
2531                        let entry_id = entry.to_proto();
2532                        menu = menu
2533                            .separator()
2534                            .when_some(entry_abs_path, |menu, abs_path| {
2535                                menu.entry(
2536                                    "Copy Path",
2537                                    Some(Box::new(zed_actions::workspace::CopyPath)),
2538                                    window.handler_for(&pane, move |_, _, cx| {
2539                                        cx.write_to_clipboard(ClipboardItem::new_string(
2540                                            abs_path.to_string_lossy().to_string(),
2541                                        ));
2542                                    }),
2543                                )
2544                            })
2545                            .when_some(relative_path, |menu, relative_path| {
2546                                menu.entry(
2547                                    "Copy Relative Path",
2548                                    Some(Box::new(zed_actions::workspace::CopyRelativePath)),
2549                                    window.handler_for(&pane, move |_, _, cx| {
2550                                        cx.write_to_clipboard(ClipboardItem::new_string(
2551                                            relative_path.to_string_lossy().to_string(),
2552                                        ));
2553                                    }),
2554                                )
2555                            })
2556                            .map(pin_tab_entries)
2557                            .separator()
2558                            .when(visible_in_project_panel, |menu| {
2559                                menu.entry(
2560                                    "Reveal In Project Panel",
2561                                    Some(Box::new(RevealInProjectPanel {
2562                                        entry_id: Some(entry_id),
2563                                    })),
2564                                    window.handler_for(&pane, move |pane, _, cx| {
2565                                        pane.project
2566                                            .update(cx, |_, cx| {
2567                                                cx.emit(project::Event::RevealInProjectPanel(
2568                                                    ProjectEntryId::from_proto(entry_id),
2569                                                ))
2570                                            })
2571                                            .ok();
2572                                    }),
2573                                )
2574                            })
2575                            .when_some(parent_abs_path, |menu, parent_abs_path| {
2576                                menu.entry(
2577                                    "Open in Terminal",
2578                                    Some(Box::new(OpenInTerminal)),
2579                                    window.handler_for(&pane, move |_, window, cx| {
2580                                        window.dispatch_action(
2581                                            OpenTerminal {
2582                                                working_directory: parent_abs_path.clone(),
2583                                            }
2584                                            .boxed_clone(),
2585                                            cx,
2586                                        );
2587                                    }),
2588                                )
2589                            });
2590                    } else {
2591                        menu = menu.map(pin_tab_entries);
2592                    }
2593                }
2594
2595                menu.context(menu_context)
2596            })
2597        })
2598    }
2599
2600    fn render_tab_bar(&mut self, window: &mut Window, cx: &mut Context<Pane>) -> AnyElement {
2601        let focus_handle = self.focus_handle.clone();
2602        let navigate_backward = IconButton::new("navigate_backward", IconName::ArrowLeft)
2603            .icon_size(IconSize::Small)
2604            .on_click({
2605                let entity = cx.entity().clone();
2606                move |_, window, cx| {
2607                    entity.update(cx, |pane, cx| pane.navigate_backward(window, cx))
2608                }
2609            })
2610            .disabled(!self.can_navigate_backward())
2611            .tooltip({
2612                let focus_handle = focus_handle.clone();
2613                move |window, cx| {
2614                    Tooltip::for_action_in("Go Back", &GoBack, &focus_handle, window, cx)
2615                }
2616            });
2617
2618        let navigate_forward = IconButton::new("navigate_forward", IconName::ArrowRight)
2619            .icon_size(IconSize::Small)
2620            .on_click({
2621                let entity = cx.entity().clone();
2622                move |_, window, cx| entity.update(cx, |pane, cx| pane.navigate_forward(window, cx))
2623            })
2624            .disabled(!self.can_navigate_forward())
2625            .tooltip({
2626                let focus_handle = focus_handle.clone();
2627                move |window, cx| {
2628                    Tooltip::for_action_in("Go Forward", &GoForward, &focus_handle, window, cx)
2629                }
2630            });
2631
2632        let mut tab_items = self
2633            .items
2634            .iter()
2635            .enumerate()
2636            .zip(tab_details(&self.items, cx))
2637            .map(|((ix, item), detail)| {
2638                self.render_tab(ix, &**item, detail, &focus_handle, window, cx)
2639            })
2640            .collect::<Vec<_>>();
2641        let tab_count = tab_items.len();
2642        let unpinned_tabs = tab_items.split_off(self.pinned_tab_count);
2643        let pinned_tabs = tab_items;
2644        TabBar::new("tab_bar")
2645            .when(
2646                self.display_nav_history_buttons.unwrap_or_default(),
2647                |tab_bar| {
2648                    tab_bar
2649                        .start_child(navigate_backward)
2650                        .start_child(navigate_forward)
2651                },
2652            )
2653            .map(|tab_bar| {
2654                if self.show_tab_bar_buttons {
2655                    let render_tab_buttons = self.render_tab_bar_buttons.clone();
2656                    let (left_children, right_children) = render_tab_buttons(self, window, cx);
2657                    tab_bar
2658                        .start_children(left_children)
2659                        .end_children(right_children)
2660                } else {
2661                    tab_bar
2662                }
2663            })
2664            .children(pinned_tabs.len().ne(&0).then(|| {
2665                let content_width = self
2666                    .tab_bar_scroll_handle
2667                    .content_size()
2668                    .map(|content_size| content_size.size.width)
2669                    .unwrap_or(px(0.));
2670                let viewport_width = self.tab_bar_scroll_handle.viewport().size.width;
2671                // We need to check both because offset returns delta values even when the scroll handle is not scrollable
2672                let is_scrollable = content_width > viewport_width;
2673                let is_scrolled = self.tab_bar_scroll_handle.offset().x < px(0.);
2674                h_flex()
2675                    .children(pinned_tabs)
2676                    .when(is_scrollable && is_scrolled, |this| {
2677                        this.border_r_1().border_color(cx.theme().colors().border)
2678                    })
2679            }))
2680            .child(
2681                h_flex()
2682                    .id("unpinned tabs")
2683                    .overflow_x_scroll()
2684                    .w_full()
2685                    .track_scroll(&self.tab_bar_scroll_handle)
2686                    .children(unpinned_tabs)
2687                    .child(
2688                        div()
2689                            .id("tab_bar_drop_target")
2690                            .min_w_6()
2691                            // HACK: This empty child is currently necessary to force the drop target to appear
2692                            // despite us setting a min width above.
2693                            .child("")
2694                            .h_full()
2695                            .flex_grow()
2696                            .drag_over::<DraggedTab>(|bar, _, _, cx| {
2697                                bar.bg(cx.theme().colors().drop_target_background)
2698                            })
2699                            .drag_over::<DraggedSelection>(|bar, _, _, cx| {
2700                                bar.bg(cx.theme().colors().drop_target_background)
2701                            })
2702                            .on_drop(cx.listener(
2703                                move |this, dragged_tab: &DraggedTab, window, cx| {
2704                                    this.drag_split_direction = None;
2705                                    this.handle_tab_drop(dragged_tab, this.items.len(), window, cx)
2706                                },
2707                            ))
2708                            .on_drop(cx.listener(
2709                                move |this, selection: &DraggedSelection, window, cx| {
2710                                    this.drag_split_direction = None;
2711                                    this.handle_project_entry_drop(
2712                                        &selection.active_selection.entry_id,
2713                                        Some(tab_count),
2714                                        window,
2715                                        cx,
2716                                    )
2717                                },
2718                            ))
2719                            .on_drop(cx.listener(move |this, paths, window, cx| {
2720                                this.drag_split_direction = None;
2721                                this.handle_external_paths_drop(paths, window, cx)
2722                            }))
2723                            .on_click(cx.listener(move |this, event: &ClickEvent, window, cx| {
2724                                if event.up.click_count == 2 {
2725                                    window.dispatch_action(
2726                                        this.double_click_dispatch_action.boxed_clone(),
2727                                        cx,
2728                                    );
2729                                }
2730                            })),
2731                    ),
2732            )
2733            .into_any_element()
2734    }
2735
2736    pub fn render_menu_overlay(menu: &Entity<ContextMenu>) -> Div {
2737        div().absolute().bottom_0().right_0().size_0().child(
2738            deferred(anchored().anchor(Corner::TopRight).child(menu.clone())).with_priority(1),
2739        )
2740    }
2741
2742    pub fn set_zoomed(&mut self, zoomed: bool, cx: &mut Context<Self>) {
2743        self.zoomed = zoomed;
2744        cx.notify();
2745    }
2746
2747    pub fn is_zoomed(&self) -> bool {
2748        self.zoomed
2749    }
2750
2751    fn handle_drag_move<T: 'static>(
2752        &mut self,
2753        event: &DragMoveEvent<T>,
2754        window: &mut Window,
2755        cx: &mut Context<Self>,
2756    ) {
2757        let can_split_predicate = self.can_split_predicate.take();
2758        let can_split = match &can_split_predicate {
2759            Some(can_split_predicate) => {
2760                can_split_predicate(self, event.dragged_item(), window, cx)
2761            }
2762            None => false,
2763        };
2764        self.can_split_predicate = can_split_predicate;
2765        if !can_split {
2766            return;
2767        }
2768
2769        let rect = event.bounds.size;
2770
2771        let size = event.bounds.size.width.min(event.bounds.size.height)
2772            * WorkspaceSettings::get_global(cx).drop_target_size;
2773
2774        let relative_cursor = Point::new(
2775            event.event.position.x - event.bounds.left(),
2776            event.event.position.y - event.bounds.top(),
2777        );
2778
2779        let direction = if relative_cursor.x < size
2780            || relative_cursor.x > rect.width - size
2781            || relative_cursor.y < size
2782            || relative_cursor.y > rect.height - size
2783        {
2784            [
2785                SplitDirection::Up,
2786                SplitDirection::Right,
2787                SplitDirection::Down,
2788                SplitDirection::Left,
2789            ]
2790            .iter()
2791            .min_by_key(|side| match side {
2792                SplitDirection::Up => relative_cursor.y,
2793                SplitDirection::Right => rect.width - relative_cursor.x,
2794                SplitDirection::Down => rect.height - relative_cursor.y,
2795                SplitDirection::Left => relative_cursor.x,
2796            })
2797            .cloned()
2798        } else {
2799            None
2800        };
2801
2802        if direction != self.drag_split_direction {
2803            self.drag_split_direction = direction;
2804        }
2805    }
2806
2807    pub fn handle_tab_drop(
2808        &mut self,
2809        dragged_tab: &DraggedTab,
2810        ix: usize,
2811        window: &mut Window,
2812        cx: &mut Context<Self>,
2813    ) {
2814        if let Some(custom_drop_handle) = self.custom_drop_handle.clone() {
2815            if let ControlFlow::Break(()) = custom_drop_handle(self, dragged_tab, window, cx) {
2816                return;
2817            }
2818        }
2819        let mut to_pane = cx.entity().clone();
2820        let split_direction = self.drag_split_direction;
2821        let item_id = dragged_tab.item.item_id();
2822        if let Some(preview_item_id) = self.preview_item_id {
2823            if item_id == preview_item_id {
2824                self.set_preview_item_id(None, cx);
2825            }
2826        }
2827
2828        let from_pane = dragged_tab.pane.clone();
2829        self.workspace
2830            .update(cx, |_, cx| {
2831                cx.defer_in(window, move |workspace, window, cx| {
2832                    if let Some(split_direction) = split_direction {
2833                        to_pane = workspace.split_pane(to_pane, split_direction, window, cx);
2834                    }
2835                    let old_ix = from_pane.read(cx).index_for_item_id(item_id);
2836                    let old_len = to_pane.read(cx).items.len();
2837                    move_item(&from_pane, &to_pane, item_id, ix, window, cx);
2838                    if to_pane == from_pane {
2839                        if let Some(old_index) = old_ix {
2840                            to_pane.update(cx, |this, _| {
2841                                if old_index < this.pinned_tab_count
2842                                    && (ix == this.items.len() || ix > this.pinned_tab_count)
2843                                {
2844                                    this.pinned_tab_count -= 1;
2845                                } else if this.has_pinned_tabs()
2846                                    && old_index >= this.pinned_tab_count
2847                                    && ix < this.pinned_tab_count
2848                                {
2849                                    this.pinned_tab_count += 1;
2850                                }
2851                            });
2852                        }
2853                    } else {
2854                        to_pane.update(cx, |this, _| {
2855                            if this.items.len() > old_len // Did we not deduplicate on drag?
2856                                && this.has_pinned_tabs()
2857                                && ix < this.pinned_tab_count
2858                            {
2859                                this.pinned_tab_count += 1;
2860                            }
2861                        });
2862                        from_pane.update(cx, |this, _| {
2863                            if let Some(index) = old_ix {
2864                                if this.pinned_tab_count > index {
2865                                    this.pinned_tab_count -= 1;
2866                                }
2867                            }
2868                        })
2869                    }
2870                });
2871            })
2872            .log_err();
2873    }
2874
2875    fn handle_dragged_selection_drop(
2876        &mut self,
2877        dragged_selection: &DraggedSelection,
2878        dragged_onto: Option<usize>,
2879        window: &mut Window,
2880        cx: &mut Context<Self>,
2881    ) {
2882        if let Some(custom_drop_handle) = self.custom_drop_handle.clone() {
2883            if let ControlFlow::Break(()) = custom_drop_handle(self, dragged_selection, window, cx)
2884            {
2885                return;
2886            }
2887        }
2888        self.handle_project_entry_drop(
2889            &dragged_selection.active_selection.entry_id,
2890            dragged_onto,
2891            window,
2892            cx,
2893        );
2894    }
2895
2896    fn handle_project_entry_drop(
2897        &mut self,
2898        project_entry_id: &ProjectEntryId,
2899        target: Option<usize>,
2900        window: &mut Window,
2901        cx: &mut Context<Self>,
2902    ) {
2903        if let Some(custom_drop_handle) = self.custom_drop_handle.clone() {
2904            if let ControlFlow::Break(()) = custom_drop_handle(self, project_entry_id, window, cx) {
2905                return;
2906            }
2907        }
2908        let mut to_pane = cx.entity().clone();
2909        let split_direction = self.drag_split_direction;
2910        let project_entry_id = *project_entry_id;
2911        self.workspace
2912            .update(cx, |_, cx| {
2913                cx.defer_in(window, move |workspace, window, cx| {
2914                    if let Some(path) = workspace
2915                        .project()
2916                        .read(cx)
2917                        .path_for_entry(project_entry_id, cx)
2918                    {
2919                        let load_path_task = workspace.load_path(path, window, cx);
2920                        cx.spawn_in(window, async move |workspace, cx| {
2921                            if let Some((project_entry_id, build_item)) =
2922                                load_path_task.await.notify_async_err(cx)
2923                            {
2924                                let (to_pane, new_item_handle) = workspace
2925                                    .update_in(cx, |workspace, window, cx| {
2926                                        if let Some(split_direction) = split_direction {
2927                                            to_pane = workspace.split_pane(
2928                                                to_pane,
2929                                                split_direction,
2930                                                window,
2931                                                cx,
2932                                            );
2933                                        }
2934                                        let new_item_handle = to_pane.update(cx, |pane, cx| {
2935                                            pane.open_item(
2936                                                project_entry_id,
2937                                                true,
2938                                                false,
2939                                                true,
2940                                                target,
2941                                                window,
2942                                                cx,
2943                                                build_item,
2944                                            )
2945                                        });
2946                                        (to_pane, new_item_handle)
2947                                    })
2948                                    .log_err()?;
2949                                to_pane
2950                                    .update_in(cx, |this, window, cx| {
2951                                        let Some(index) = this.index_for_item(&*new_item_handle)
2952                                        else {
2953                                            return;
2954                                        };
2955
2956                                        if target.map_or(false, |target| this.is_tab_pinned(target))
2957                                        {
2958                                            this.pin_tab_at(index, window, cx);
2959                                        }
2960                                    })
2961                                    .ok()?
2962                            }
2963                            Some(())
2964                        })
2965                        .detach();
2966                    };
2967                });
2968            })
2969            .log_err();
2970    }
2971
2972    fn handle_external_paths_drop(
2973        &mut self,
2974        paths: &ExternalPaths,
2975        window: &mut Window,
2976        cx: &mut Context<Self>,
2977    ) {
2978        if let Some(custom_drop_handle) = self.custom_drop_handle.clone() {
2979            if let ControlFlow::Break(()) = custom_drop_handle(self, paths, window, cx) {
2980                return;
2981            }
2982        }
2983        let mut to_pane = cx.entity().clone();
2984        let mut split_direction = self.drag_split_direction;
2985        let paths = paths.paths().to_vec();
2986        let is_remote = self
2987            .workspace
2988            .update(cx, |workspace, cx| {
2989                if workspace.project().read(cx).is_via_collab() {
2990                    workspace.show_error(
2991                        &anyhow::anyhow!("Cannot drop files on a remote project"),
2992                        cx,
2993                    );
2994                    true
2995                } else {
2996                    false
2997                }
2998            })
2999            .unwrap_or(true);
3000        if is_remote {
3001            return;
3002        }
3003
3004        self.workspace
3005            .update(cx, |workspace, cx| {
3006                let fs = Arc::clone(workspace.project().read(cx).fs());
3007                cx.spawn_in(window, async move |workspace, cx| {
3008                    let mut is_file_checks = FuturesUnordered::new();
3009                    for path in &paths {
3010                        is_file_checks.push(fs.is_file(path))
3011                    }
3012                    let mut has_files_to_open = false;
3013                    while let Some(is_file) = is_file_checks.next().await {
3014                        if is_file {
3015                            has_files_to_open = true;
3016                            break;
3017                        }
3018                    }
3019                    drop(is_file_checks);
3020                    if !has_files_to_open {
3021                        split_direction = None;
3022                    }
3023
3024                    if let Ok(open_task) = workspace.update_in(cx, |workspace, window, cx| {
3025                        if let Some(split_direction) = split_direction {
3026                            to_pane = workspace.split_pane(to_pane, split_direction, window, cx);
3027                        }
3028                        workspace.open_paths(
3029                            paths,
3030                            OpenOptions {
3031                                visible: Some(OpenVisible::OnlyDirectories),
3032                                ..Default::default()
3033                            },
3034                            Some(to_pane.downgrade()),
3035                            window,
3036                            cx,
3037                        )
3038                    }) {
3039                        let opened_items: Vec<_> = open_task.await;
3040                        _ = workspace.update(cx, |workspace, cx| {
3041                            for item in opened_items.into_iter().flatten() {
3042                                if let Err(e) = item {
3043                                    workspace.show_error(&e, cx);
3044                                }
3045                            }
3046                        });
3047                    }
3048                })
3049                .detach();
3050            })
3051            .log_err();
3052    }
3053
3054    pub fn display_nav_history_buttons(&mut self, display: Option<bool>) {
3055        self.display_nav_history_buttons = display;
3056    }
3057
3058    fn get_non_closeable_item_ids(&self, close_pinned: bool) -> Vec<EntityId> {
3059        if close_pinned {
3060            return vec![];
3061        }
3062
3063        self.items
3064            .iter()
3065            .enumerate()
3066            .filter(|(index, _item)| self.is_tab_pinned(*index))
3067            .map(|(_, item)| item.item_id())
3068            .collect()
3069    }
3070
3071    pub fn drag_split_direction(&self) -> Option<SplitDirection> {
3072        self.drag_split_direction
3073    }
3074
3075    pub fn set_zoom_out_on_close(&mut self, zoom_out_on_close: bool) {
3076        self.zoom_out_on_close = zoom_out_on_close;
3077    }
3078}
3079
3080fn default_render_tab_bar_buttons(
3081    pane: &mut Pane,
3082    window: &mut Window,
3083    cx: &mut Context<Pane>,
3084) -> (Option<AnyElement>, Option<AnyElement>) {
3085    if !pane.has_focus(window, cx) && !pane.context_menu_focused(window, cx) {
3086        return (None, None);
3087    }
3088    // Ideally we would return a vec of elements here to pass directly to the [TabBar]'s
3089    // `end_slot`, but due to needing a view here that isn't possible.
3090    let right_children = h_flex()
3091        // Instead we need to replicate the spacing from the [TabBar]'s `end_slot` here.
3092        .gap(DynamicSpacing::Base04.rems(cx))
3093        .child(
3094            PopoverMenu::new("pane-tab-bar-popover-menu")
3095                .trigger_with_tooltip(
3096                    IconButton::new("plus", IconName::Plus).icon_size(IconSize::Small),
3097                    Tooltip::text("New..."),
3098                )
3099                .anchor(Corner::TopRight)
3100                .with_handle(pane.new_item_context_menu_handle.clone())
3101                .menu(move |window, cx| {
3102                    Some(ContextMenu::build(window, cx, |menu, _, _| {
3103                        menu.action("New File", NewFile.boxed_clone())
3104                            .action("Open File", ToggleFileFinder::default().boxed_clone())
3105                            .separator()
3106                            .action(
3107                                "Search Project",
3108                                DeploySearch {
3109                                    replace_enabled: false,
3110                                }
3111                                .boxed_clone(),
3112                            )
3113                            .action("Search Symbols", ToggleProjectSymbols.boxed_clone())
3114                            .separator()
3115                            .action("New Terminal", NewTerminal.boxed_clone())
3116                    }))
3117                }),
3118        )
3119        .child(
3120            PopoverMenu::new("pane-tab-bar-split")
3121                .trigger_with_tooltip(
3122                    IconButton::new("split", IconName::Split).icon_size(IconSize::Small),
3123                    Tooltip::text("Split Pane"),
3124                )
3125                .anchor(Corner::TopRight)
3126                .with_handle(pane.split_item_context_menu_handle.clone())
3127                .menu(move |window, cx| {
3128                    ContextMenu::build(window, cx, |menu, _, _| {
3129                        menu.action("Split Right", SplitRight.boxed_clone())
3130                            .action("Split Left", SplitLeft.boxed_clone())
3131                            .action("Split Up", SplitUp.boxed_clone())
3132                            .action("Split Down", SplitDown.boxed_clone())
3133                    })
3134                    .into()
3135                }),
3136        )
3137        .child({
3138            let zoomed = pane.is_zoomed();
3139            IconButton::new("toggle_zoom", IconName::Maximize)
3140                .icon_size(IconSize::Small)
3141                .toggle_state(zoomed)
3142                .selected_icon(IconName::Minimize)
3143                .on_click(cx.listener(|pane, _, window, cx| {
3144                    pane.toggle_zoom(&crate::ToggleZoom, window, cx);
3145                }))
3146                .tooltip(move |window, cx| {
3147                    Tooltip::for_action(
3148                        if zoomed { "Zoom Out" } else { "Zoom In" },
3149                        &ToggleZoom,
3150                        window,
3151                        cx,
3152                    )
3153                })
3154        })
3155        .into_any_element()
3156        .into();
3157    (None, right_children)
3158}
3159
3160impl Focusable for Pane {
3161    fn focus_handle(&self, _cx: &App) -> FocusHandle {
3162        self.focus_handle.clone()
3163    }
3164}
3165
3166impl Render for Pane {
3167    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
3168        let mut key_context = KeyContext::new_with_defaults();
3169        key_context.add("Pane");
3170        if self.active_item().is_none() {
3171            key_context.add("EmptyPane");
3172        }
3173
3174        let should_display_tab_bar = self.should_display_tab_bar.clone();
3175        let display_tab_bar = should_display_tab_bar(window, cx);
3176        let Some(project) = self.project.upgrade() else {
3177            return div().track_focus(&self.focus_handle(cx));
3178        };
3179        let is_local = project.read(cx).is_local();
3180
3181        v_flex()
3182            .key_context(key_context)
3183            .track_focus(&self.focus_handle(cx))
3184            .size_full()
3185            .flex_none()
3186            .overflow_hidden()
3187            .on_action(cx.listener(|pane, _: &AlternateFile, window, cx| {
3188                pane.alternate_file(window, cx);
3189            }))
3190            .on_action(
3191                cx.listener(|pane, _: &SplitLeft, _, cx| pane.split(SplitDirection::Left, cx)),
3192            )
3193            .on_action(cx.listener(|pane, _: &SplitUp, _, cx| pane.split(SplitDirection::Up, cx)))
3194            .on_action(cx.listener(|pane, _: &SplitHorizontal, _, cx| {
3195                pane.split(SplitDirection::horizontal(cx), cx)
3196            }))
3197            .on_action(cx.listener(|pane, _: &SplitVertical, _, cx| {
3198                pane.split(SplitDirection::vertical(cx), cx)
3199            }))
3200            .on_action(
3201                cx.listener(|pane, _: &SplitRight, _, cx| pane.split(SplitDirection::Right, cx)),
3202            )
3203            .on_action(
3204                cx.listener(|pane, _: &SplitDown, _, cx| pane.split(SplitDirection::Down, cx)),
3205            )
3206            .on_action(
3207                cx.listener(|pane, _: &GoBack, window, cx| pane.navigate_backward(window, cx)),
3208            )
3209            .on_action(
3210                cx.listener(|pane, _: &GoForward, window, cx| pane.navigate_forward(window, cx)),
3211            )
3212            .on_action(cx.listener(|_, _: &JoinIntoNext, _, cx| {
3213                cx.emit(Event::JoinIntoNext);
3214            }))
3215            .on_action(cx.listener(|_, _: &JoinAll, _, cx| {
3216                cx.emit(Event::JoinAll);
3217            }))
3218            .on_action(cx.listener(Pane::toggle_zoom))
3219            .on_action(
3220                cx.listener(|pane: &mut Pane, action: &ActivateItem, window, cx| {
3221                    pane.activate_item(action.0, true, true, window, cx);
3222                }),
3223            )
3224            .on_action(
3225                cx.listener(|pane: &mut Pane, _: &ActivateLastItem, window, cx| {
3226                    pane.activate_item(pane.items.len() - 1, true, true, window, cx);
3227                }),
3228            )
3229            .on_action(
3230                cx.listener(|pane: &mut Pane, _: &ActivatePreviousItem, window, cx| {
3231                    pane.activate_prev_item(true, window, cx);
3232                }),
3233            )
3234            .on_action(
3235                cx.listener(|pane: &mut Pane, _: &ActivateNextItem, window, cx| {
3236                    pane.activate_next_item(true, window, cx);
3237                }),
3238            )
3239            .on_action(
3240                cx.listener(|pane, _: &SwapItemLeft, window, cx| pane.swap_item_left(window, cx)),
3241            )
3242            .on_action(
3243                cx.listener(|pane, _: &SwapItemRight, window, cx| pane.swap_item_right(window, cx)),
3244            )
3245            .on_action(cx.listener(|pane, action, window, cx| {
3246                pane.toggle_pin_tab(action, window, cx);
3247            }))
3248            .when(PreviewTabsSettings::get_global(cx).enabled, |this| {
3249                this.on_action(cx.listener(|pane: &mut Pane, _: &TogglePreviewTab, _, cx| {
3250                    if let Some(active_item_id) = pane.active_item().map(|i| i.item_id()) {
3251                        if pane.is_active_preview_item(active_item_id) {
3252                            pane.set_preview_item_id(None, cx);
3253                        } else {
3254                            pane.set_preview_item_id(Some(active_item_id), cx);
3255                        }
3256                    }
3257                }))
3258            })
3259            .on_action(
3260                cx.listener(|pane: &mut Self, action: &CloseActiveItem, window, cx| {
3261                    if let Some(task) = pane.close_active_item(action, window, cx) {
3262                        task.detach_and_log_err(cx)
3263                    }
3264                }),
3265            )
3266            .on_action(
3267                cx.listener(|pane: &mut Self, action: &CloseInactiveItems, window, cx| {
3268                    if let Some(task) = pane.close_inactive_items(action, window, cx) {
3269                        task.detach_and_log_err(cx)
3270                    }
3271                }),
3272            )
3273            .on_action(
3274                cx.listener(|pane: &mut Self, action: &CloseCleanItems, window, cx| {
3275                    if let Some(task) = pane.close_clean_items(action, window, cx) {
3276                        task.detach_and_log_err(cx)
3277                    }
3278                }),
3279            )
3280            .on_action(cx.listener(
3281                |pane: &mut Self, action: &CloseItemsToTheLeft, window, cx| {
3282                    if let Some(task) = pane.close_items_to_the_left(action, window, cx) {
3283                        task.detach_and_log_err(cx)
3284                    }
3285                },
3286            ))
3287            .on_action(cx.listener(
3288                |pane: &mut Self, action: &CloseItemsToTheRight, window, cx| {
3289                    if let Some(task) = pane.close_items_to_the_right(action, window, cx) {
3290                        task.detach_and_log_err(cx)
3291                    }
3292                },
3293            ))
3294            .on_action(
3295                cx.listener(|pane: &mut Self, action: &CloseAllItems, window, cx| {
3296                    if let Some(task) = pane.close_all_items(action, window, cx) {
3297                        task.detach_and_log_err(cx)
3298                    }
3299                }),
3300            )
3301            .on_action(
3302                cx.listener(|pane: &mut Self, action: &CloseActiveItem, window, cx| {
3303                    if let Some(task) = pane.close_active_item(action, window, cx) {
3304                        task.detach_and_log_err(cx)
3305                    }
3306                }),
3307            )
3308            .on_action(
3309                cx.listener(|pane: &mut Self, action: &RevealInProjectPanel, _, cx| {
3310                    let entry_id = action
3311                        .entry_id
3312                        .map(ProjectEntryId::from_proto)
3313                        .or_else(|| pane.active_item()?.project_entry_ids(cx).first().copied());
3314                    if let Some(entry_id) = entry_id {
3315                        pane.project
3316                            .update(cx, |_, cx| {
3317                                cx.emit(project::Event::RevealInProjectPanel(entry_id))
3318                            })
3319                            .ok();
3320                    }
3321                }),
3322            )
3323            .when(self.active_item().is_some() && display_tab_bar, |pane| {
3324                pane.child((self.render_tab_bar.clone())(self, window, cx))
3325            })
3326            .child({
3327                let has_worktrees = project.read(cx).visible_worktrees(cx).next().is_some();
3328                // main content
3329                div()
3330                    .flex_1()
3331                    .relative()
3332                    .group("")
3333                    .overflow_hidden()
3334                    .on_drag_move::<DraggedTab>(cx.listener(Self::handle_drag_move))
3335                    .on_drag_move::<DraggedSelection>(cx.listener(Self::handle_drag_move))
3336                    .when(is_local, |div| {
3337                        div.on_drag_move::<ExternalPaths>(cx.listener(Self::handle_drag_move))
3338                    })
3339                    .map(|div| {
3340                        if let Some(item) = self.active_item() {
3341                            div.id("pane_placeholder")
3342                                .v_flex()
3343                                .size_full()
3344                                .overflow_hidden()
3345                                .child(self.toolbar.clone())
3346                                .child(item.to_any())
3347                        } else {
3348                            let placeholder = div
3349                                .id("pane_placeholder")
3350                                .h_flex()
3351                                .size_full()
3352                                .justify_center()
3353                                .on_click(cx.listener(
3354                                    move |this, event: &ClickEvent, window, cx| {
3355                                        if event.up.click_count == 2 {
3356                                            window.dispatch_action(
3357                                                this.double_click_dispatch_action.boxed_clone(),
3358                                                cx,
3359                                            );
3360                                        }
3361                                    },
3362                                ));
3363                            if has_worktrees {
3364                                placeholder
3365                            } else {
3366                                placeholder.child(
3367                                    Label::new("Open a file or project to get started.")
3368                                        .color(Color::Muted),
3369                                )
3370                            }
3371                        }
3372                    })
3373                    .child(
3374                        // drag target
3375                        div()
3376                            .invisible()
3377                            .absolute()
3378                            .bg(cx.theme().colors().drop_target_background)
3379                            .group_drag_over::<DraggedTab>("", |style| style.visible())
3380                            .group_drag_over::<DraggedSelection>("", |style| style.visible())
3381                            .when(is_local, |div| {
3382                                div.group_drag_over::<ExternalPaths>("", |style| style.visible())
3383                            })
3384                            .when_some(self.can_drop_predicate.clone(), |this, p| {
3385                                this.can_drop(move |a, window, cx| p(a, window, cx))
3386                            })
3387                            .on_drop(cx.listener(move |this, dragged_tab, window, cx| {
3388                                this.handle_tab_drop(
3389                                    dragged_tab,
3390                                    this.active_item_index(),
3391                                    window,
3392                                    cx,
3393                                )
3394                            }))
3395                            .on_drop(cx.listener(
3396                                move |this, selection: &DraggedSelection, window, cx| {
3397                                    this.handle_dragged_selection_drop(selection, None, window, cx)
3398                                },
3399                            ))
3400                            .on_drop(cx.listener(move |this, paths, window, cx| {
3401                                this.handle_external_paths_drop(paths, window, cx)
3402                            }))
3403                            .map(|div| {
3404                                let size = DefiniteLength::Fraction(0.5);
3405                                match self.drag_split_direction {
3406                                    None => div.top_0().right_0().bottom_0().left_0(),
3407                                    Some(SplitDirection::Up) => {
3408                                        div.top_0().left_0().right_0().h(size)
3409                                    }
3410                                    Some(SplitDirection::Down) => {
3411                                        div.left_0().bottom_0().right_0().h(size)
3412                                    }
3413                                    Some(SplitDirection::Left) => {
3414                                        div.top_0().left_0().bottom_0().w(size)
3415                                    }
3416                                    Some(SplitDirection::Right) => {
3417                                        div.top_0().bottom_0().right_0().w(size)
3418                                    }
3419                                }
3420                            }),
3421                    )
3422            })
3423            .on_mouse_down(
3424                MouseButton::Navigate(NavigationDirection::Back),
3425                cx.listener(|pane, _, window, cx| {
3426                    if let Some(workspace) = pane.workspace.upgrade() {
3427                        let pane = cx.entity().downgrade();
3428                        window.defer(cx, move |window, cx| {
3429                            workspace.update(cx, |workspace, cx| {
3430                                workspace.go_back(pane, window, cx).detach_and_log_err(cx)
3431                            })
3432                        })
3433                    }
3434                }),
3435            )
3436            .on_mouse_down(
3437                MouseButton::Navigate(NavigationDirection::Forward),
3438                cx.listener(|pane, _, window, cx| {
3439                    if let Some(workspace) = pane.workspace.upgrade() {
3440                        let pane = cx.entity().downgrade();
3441                        window.defer(cx, move |window, cx| {
3442                            workspace.update(cx, |workspace, cx| {
3443                                workspace
3444                                    .go_forward(pane, window, cx)
3445                                    .detach_and_log_err(cx)
3446                            })
3447                        })
3448                    }
3449                }),
3450            )
3451    }
3452}
3453
3454impl ItemNavHistory {
3455    pub fn push<D: 'static + Send + Any>(&mut self, data: Option<D>, cx: &mut App) {
3456        if self
3457            .item
3458            .upgrade()
3459            .is_some_and(|item| item.include_in_nav_history())
3460        {
3461            self.history
3462                .push(data, self.item.clone(), self.is_preview, cx);
3463        }
3464    }
3465
3466    pub fn pop_backward(&mut self, cx: &mut App) -> Option<NavigationEntry> {
3467        self.history.pop(NavigationMode::GoingBack, cx)
3468    }
3469
3470    pub fn pop_forward(&mut self, cx: &mut App) -> Option<NavigationEntry> {
3471        self.history.pop(NavigationMode::GoingForward, cx)
3472    }
3473}
3474
3475impl NavHistory {
3476    pub fn for_each_entry(
3477        &self,
3478        cx: &App,
3479        mut f: impl FnMut(&NavigationEntry, (ProjectPath, Option<PathBuf>)),
3480    ) {
3481        let borrowed_history = self.0.lock();
3482        borrowed_history
3483            .forward_stack
3484            .iter()
3485            .chain(borrowed_history.backward_stack.iter())
3486            .chain(borrowed_history.closed_stack.iter())
3487            .for_each(|entry| {
3488                if let Some(project_and_abs_path) =
3489                    borrowed_history.paths_by_item.get(&entry.item.id())
3490                {
3491                    f(entry, project_and_abs_path.clone());
3492                } else if let Some(item) = entry.item.upgrade() {
3493                    if let Some(path) = item.project_path(cx) {
3494                        f(entry, (path, None));
3495                    }
3496                }
3497            })
3498    }
3499
3500    pub fn set_mode(&mut self, mode: NavigationMode) {
3501        self.0.lock().mode = mode;
3502    }
3503
3504    pub fn mode(&self) -> NavigationMode {
3505        self.0.lock().mode
3506    }
3507
3508    pub fn disable(&mut self) {
3509        self.0.lock().mode = NavigationMode::Disabled;
3510    }
3511
3512    pub fn enable(&mut self) {
3513        self.0.lock().mode = NavigationMode::Normal;
3514    }
3515
3516    pub fn pop(&mut self, mode: NavigationMode, cx: &mut App) -> Option<NavigationEntry> {
3517        let mut state = self.0.lock();
3518        let entry = match mode {
3519            NavigationMode::Normal | NavigationMode::Disabled | NavigationMode::ClosingItem => {
3520                return None;
3521            }
3522            NavigationMode::GoingBack => &mut state.backward_stack,
3523            NavigationMode::GoingForward => &mut state.forward_stack,
3524            NavigationMode::ReopeningClosedItem => &mut state.closed_stack,
3525        }
3526        .pop_back();
3527        if entry.is_some() {
3528            state.did_update(cx);
3529        }
3530        entry
3531    }
3532
3533    pub fn push<D: 'static + Send + Any>(
3534        &mut self,
3535        data: Option<D>,
3536        item: Arc<dyn WeakItemHandle>,
3537        is_preview: bool,
3538        cx: &mut App,
3539    ) {
3540        let state = &mut *self.0.lock();
3541        match state.mode {
3542            NavigationMode::Disabled => {}
3543            NavigationMode::Normal | NavigationMode::ReopeningClosedItem => {
3544                if state.backward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
3545                    state.backward_stack.pop_front();
3546                }
3547                state.backward_stack.push_back(NavigationEntry {
3548                    item,
3549                    data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
3550                    timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
3551                    is_preview,
3552                });
3553                state.forward_stack.clear();
3554            }
3555            NavigationMode::GoingBack => {
3556                if state.forward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
3557                    state.forward_stack.pop_front();
3558                }
3559                state.forward_stack.push_back(NavigationEntry {
3560                    item,
3561                    data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
3562                    timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
3563                    is_preview,
3564                });
3565            }
3566            NavigationMode::GoingForward => {
3567                if state.backward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
3568                    state.backward_stack.pop_front();
3569                }
3570                state.backward_stack.push_back(NavigationEntry {
3571                    item,
3572                    data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
3573                    timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
3574                    is_preview,
3575                });
3576            }
3577            NavigationMode::ClosingItem => {
3578                if state.closed_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
3579                    state.closed_stack.pop_front();
3580                }
3581                state.closed_stack.push_back(NavigationEntry {
3582                    item,
3583                    data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
3584                    timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
3585                    is_preview,
3586                });
3587            }
3588        }
3589        state.did_update(cx);
3590    }
3591
3592    pub fn remove_item(&mut self, item_id: EntityId) {
3593        let mut state = self.0.lock();
3594        state.paths_by_item.remove(&item_id);
3595        state
3596            .backward_stack
3597            .retain(|entry| entry.item.id() != item_id);
3598        state
3599            .forward_stack
3600            .retain(|entry| entry.item.id() != item_id);
3601        state
3602            .closed_stack
3603            .retain(|entry| entry.item.id() != item_id);
3604    }
3605
3606    pub fn path_for_item(&self, item_id: EntityId) -> Option<(ProjectPath, Option<PathBuf>)> {
3607        self.0.lock().paths_by_item.get(&item_id).cloned()
3608    }
3609}
3610
3611impl NavHistoryState {
3612    pub fn did_update(&self, cx: &mut App) {
3613        if let Some(pane) = self.pane.upgrade() {
3614            cx.defer(move |cx| {
3615                pane.update(cx, |pane, cx| pane.history_updated(cx));
3616            });
3617        }
3618    }
3619}
3620
3621fn dirty_message_for(buffer_path: Option<ProjectPath>) -> String {
3622    let path = buffer_path
3623        .as_ref()
3624        .and_then(|p| {
3625            p.path
3626                .to_str()
3627                .and_then(|s| if s.is_empty() { None } else { Some(s) })
3628        })
3629        .unwrap_or("This buffer");
3630    let path = truncate_and_remove_front(path, 80);
3631    format!("{path} contains unsaved edits. Do you want to save it?")
3632}
3633
3634pub fn tab_details(items: &[Box<dyn ItemHandle>], cx: &App) -> Vec<usize> {
3635    let mut tab_details = items.iter().map(|_| 0).collect::<Vec<_>>();
3636    let mut tab_descriptions = HashMap::default();
3637    let mut done = false;
3638    while !done {
3639        done = true;
3640
3641        // Store item indices by their tab description.
3642        for (ix, (item, detail)) in items.iter().zip(&tab_details).enumerate() {
3643            if let Some(description) = item.tab_description(*detail, cx) {
3644                if *detail == 0
3645                    || Some(&description) != item.tab_description(detail - 1, cx).as_ref()
3646                {
3647                    tab_descriptions
3648                        .entry(description)
3649                        .or_insert(Vec::new())
3650                        .push(ix);
3651                }
3652            }
3653        }
3654
3655        // If two or more items have the same tab description, increase their level
3656        // of detail and try again.
3657        for (_, item_ixs) in tab_descriptions.drain() {
3658            if item_ixs.len() > 1 {
3659                done = false;
3660                for ix in item_ixs {
3661                    tab_details[ix] += 1;
3662                }
3663            }
3664        }
3665    }
3666
3667    tab_details
3668}
3669
3670pub fn render_item_indicator(item: Box<dyn ItemHandle>, cx: &App) -> Option<Indicator> {
3671    maybe!({
3672        let indicator_color = match (item.has_conflict(cx), item.is_dirty(cx)) {
3673            (true, _) => Color::Warning,
3674            (_, true) => Color::Accent,
3675            (false, false) => return None,
3676        };
3677
3678        Some(Indicator::dot().color(indicator_color))
3679    })
3680}
3681
3682impl Render for DraggedTab {
3683    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
3684        let ui_font = ThemeSettings::get_global(cx).ui_font.clone();
3685        let label = self.item.tab_content(
3686            TabContentParams {
3687                detail: Some(self.detail),
3688                selected: false,
3689                preview: false,
3690            },
3691            window,
3692            cx,
3693        );
3694        Tab::new("")
3695            .toggle_state(self.is_active)
3696            .child(label)
3697            .render(window, cx)
3698            .font(ui_font)
3699    }
3700}
3701
3702#[cfg(test)]
3703mod tests {
3704    use std::num::NonZero;
3705
3706    use super::*;
3707    use crate::item::test::{TestItem, TestProjectItem};
3708    use gpui::{TestAppContext, VisualTestContext};
3709    use project::FakeFs;
3710    use settings::SettingsStore;
3711    use theme::LoadThemes;
3712
3713    #[gpui::test]
3714    async fn test_remove_active_empty(cx: &mut TestAppContext) {
3715        init_test(cx);
3716        let fs = FakeFs::new(cx.executor());
3717
3718        let project = Project::test(fs, None, cx).await;
3719        let (workspace, cx) =
3720            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
3721        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3722
3723        pane.update_in(cx, |pane, window, cx| {
3724            assert!(
3725                pane.close_active_item(
3726                    &CloseActiveItem {
3727                        save_intent: None,
3728                        close_pinned: false
3729                    },
3730                    window,
3731                    cx
3732                )
3733                .is_none()
3734            )
3735        });
3736    }
3737
3738    #[gpui::test]
3739    async fn test_add_item_capped_to_max_tabs(cx: &mut TestAppContext) {
3740        init_test(cx);
3741        let fs = FakeFs::new(cx.executor());
3742
3743        let project = Project::test(fs, None, cx).await;
3744        let (workspace, cx) =
3745            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
3746        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3747
3748        for i in 0..7 {
3749            add_labeled_item(&pane, format!("{}", i).as_str(), false, cx);
3750        }
3751        set_max_tabs(cx, Some(5));
3752        add_labeled_item(&pane, "7", false, cx);
3753        // Remove items to respect the max tab cap.
3754        assert_item_labels(&pane, ["3", "4", "5", "6", "7*"], cx);
3755        pane.update_in(cx, |pane, window, cx| {
3756            pane.activate_item(0, false, false, window, cx);
3757        });
3758        add_labeled_item(&pane, "X", false, cx);
3759        // Respect activation order.
3760        assert_item_labels(&pane, ["3", "X*", "5", "6", "7"], cx);
3761
3762        for i in 0..7 {
3763            add_labeled_item(&pane, format!("D{}", i).as_str(), true, cx);
3764        }
3765        // Keeps dirty items, even over max tab cap.
3766        assert_item_labels(
3767            &pane,
3768            ["D0^", "D1^", "D2^", "D3^", "D4^", "D5^", "D6*^"],
3769            cx,
3770        );
3771
3772        set_max_tabs(cx, None);
3773        for i in 0..7 {
3774            add_labeled_item(&pane, format!("N{}", i).as_str(), false, cx);
3775        }
3776        // No cap when max tabs is None.
3777        assert_item_labels(
3778            &pane,
3779            [
3780                "D0^", "D1^", "D2^", "D3^", "D4^", "D5^", "D6^", "N0", "N1", "N2", "N3", "N4",
3781                "N5", "N6*",
3782            ],
3783            cx,
3784        );
3785    }
3786
3787    #[gpui::test]
3788    async fn test_add_item_with_new_item(cx: &mut TestAppContext) {
3789        init_test(cx);
3790        let fs = FakeFs::new(cx.executor());
3791
3792        let project = Project::test(fs, None, cx).await;
3793        let (workspace, cx) =
3794            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
3795        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3796
3797        // 1. Add with a destination index
3798        //   a. Add before the active item
3799        set_labeled_items(&pane, ["A", "B*", "C"], cx);
3800        pane.update_in(cx, |pane, window, cx| {
3801            pane.add_item(
3802                Box::new(cx.new(|cx| TestItem::new(cx).with_label("D"))),
3803                false,
3804                false,
3805                Some(0),
3806                window,
3807                cx,
3808            );
3809        });
3810        assert_item_labels(&pane, ["D*", "A", "B", "C"], cx);
3811
3812        //   b. Add after the active item
3813        set_labeled_items(&pane, ["A", "B*", "C"], cx);
3814        pane.update_in(cx, |pane, window, cx| {
3815            pane.add_item(
3816                Box::new(cx.new(|cx| TestItem::new(cx).with_label("D"))),
3817                false,
3818                false,
3819                Some(2),
3820                window,
3821                cx,
3822            );
3823        });
3824        assert_item_labels(&pane, ["A", "B", "D*", "C"], cx);
3825
3826        //   c. Add at the end of the item list (including off the length)
3827        set_labeled_items(&pane, ["A", "B*", "C"], cx);
3828        pane.update_in(cx, |pane, window, cx| {
3829            pane.add_item(
3830                Box::new(cx.new(|cx| TestItem::new(cx).with_label("D"))),
3831                false,
3832                false,
3833                Some(5),
3834                window,
3835                cx,
3836            );
3837        });
3838        assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
3839
3840        // 2. Add without a destination index
3841        //   a. Add with active item at the start of the item list
3842        set_labeled_items(&pane, ["A*", "B", "C"], cx);
3843        pane.update_in(cx, |pane, window, cx| {
3844            pane.add_item(
3845                Box::new(cx.new(|cx| TestItem::new(cx).with_label("D"))),
3846                false,
3847                false,
3848                None,
3849                window,
3850                cx,
3851            );
3852        });
3853        set_labeled_items(&pane, ["A", "D*", "B", "C"], cx);
3854
3855        //   b. Add with active item at the end of the item list
3856        set_labeled_items(&pane, ["A", "B", "C*"], cx);
3857        pane.update_in(cx, |pane, window, cx| {
3858            pane.add_item(
3859                Box::new(cx.new(|cx| TestItem::new(cx).with_label("D"))),
3860                false,
3861                false,
3862                None,
3863                window,
3864                cx,
3865            );
3866        });
3867        assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
3868    }
3869
3870    #[gpui::test]
3871    async fn test_add_item_with_existing_item(cx: &mut TestAppContext) {
3872        init_test(cx);
3873        let fs = FakeFs::new(cx.executor());
3874
3875        let project = Project::test(fs, None, cx).await;
3876        let (workspace, cx) =
3877            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
3878        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3879
3880        // 1. Add with a destination index
3881        //   1a. Add before the active item
3882        let [_, _, _, d] = set_labeled_items(&pane, ["A", "B*", "C", "D"], cx);
3883        pane.update_in(cx, |pane, window, cx| {
3884            pane.add_item(d, false, false, Some(0), window, cx);
3885        });
3886        assert_item_labels(&pane, ["D*", "A", "B", "C"], cx);
3887
3888        //   1b. Add after the active item
3889        let [_, _, _, d] = set_labeled_items(&pane, ["A", "B*", "C", "D"], cx);
3890        pane.update_in(cx, |pane, window, cx| {
3891            pane.add_item(d, false, false, Some(2), window, cx);
3892        });
3893        assert_item_labels(&pane, ["A", "B", "D*", "C"], cx);
3894
3895        //   1c. Add at the end of the item list (including off the length)
3896        let [a, _, _, _] = set_labeled_items(&pane, ["A", "B*", "C", "D"], cx);
3897        pane.update_in(cx, |pane, window, cx| {
3898            pane.add_item(a, false, false, Some(5), window, cx);
3899        });
3900        assert_item_labels(&pane, ["B", "C", "D", "A*"], cx);
3901
3902        //   1d. Add same item to active index
3903        let [_, b, _] = set_labeled_items(&pane, ["A", "B*", "C"], cx);
3904        pane.update_in(cx, |pane, window, cx| {
3905            pane.add_item(b, false, false, Some(1), window, cx);
3906        });
3907        assert_item_labels(&pane, ["A", "B*", "C"], cx);
3908
3909        //   1e. Add item to index after same item in last position
3910        let [_, _, c] = set_labeled_items(&pane, ["A", "B*", "C"], cx);
3911        pane.update_in(cx, |pane, window, cx| {
3912            pane.add_item(c, false, false, Some(2), window, cx);
3913        });
3914        assert_item_labels(&pane, ["A", "B", "C*"], cx);
3915
3916        // 2. Add without a destination index
3917        //   2a. Add with active item at the start of the item list
3918        let [_, _, _, d] = set_labeled_items(&pane, ["A*", "B", "C", "D"], cx);
3919        pane.update_in(cx, |pane, window, cx| {
3920            pane.add_item(d, false, false, None, window, cx);
3921        });
3922        assert_item_labels(&pane, ["A", "D*", "B", "C"], cx);
3923
3924        //   2b. Add with active item at the end of the item list
3925        let [a, _, _, _] = set_labeled_items(&pane, ["A", "B", "C", "D*"], cx);
3926        pane.update_in(cx, |pane, window, cx| {
3927            pane.add_item(a, false, false, None, window, cx);
3928        });
3929        assert_item_labels(&pane, ["B", "C", "D", "A*"], cx);
3930
3931        //   2c. Add active item to active item at end of list
3932        let [_, _, c] = set_labeled_items(&pane, ["A", "B", "C*"], cx);
3933        pane.update_in(cx, |pane, window, cx| {
3934            pane.add_item(c, false, false, None, window, cx);
3935        });
3936        assert_item_labels(&pane, ["A", "B", "C*"], cx);
3937
3938        //   2d. Add active item to active item at start of list
3939        let [a, _, _] = set_labeled_items(&pane, ["A*", "B", "C"], cx);
3940        pane.update_in(cx, |pane, window, cx| {
3941            pane.add_item(a, false, false, None, window, cx);
3942        });
3943        assert_item_labels(&pane, ["A*", "B", "C"], cx);
3944    }
3945
3946    #[gpui::test]
3947    async fn test_add_item_with_same_project_entries(cx: &mut TestAppContext) {
3948        init_test(cx);
3949        let fs = FakeFs::new(cx.executor());
3950
3951        let project = Project::test(fs, None, cx).await;
3952        let (workspace, cx) =
3953            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
3954        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3955
3956        // singleton view
3957        pane.update_in(cx, |pane, window, cx| {
3958            pane.add_item(
3959                Box::new(cx.new(|cx| {
3960                    TestItem::new(cx)
3961                        .with_singleton(true)
3962                        .with_label("buffer 1")
3963                        .with_project_items(&[TestProjectItem::new(1, "one.txt", cx)])
3964                })),
3965                false,
3966                false,
3967                None,
3968                window,
3969                cx,
3970            );
3971        });
3972        assert_item_labels(&pane, ["buffer 1*"], cx);
3973
3974        // new singleton view with the same project entry
3975        pane.update_in(cx, |pane, window, cx| {
3976            pane.add_item(
3977                Box::new(cx.new(|cx| {
3978                    TestItem::new(cx)
3979                        .with_singleton(true)
3980                        .with_label("buffer 1")
3981                        .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
3982                })),
3983                false,
3984                false,
3985                None,
3986                window,
3987                cx,
3988            );
3989        });
3990        assert_item_labels(&pane, ["buffer 1*"], cx);
3991
3992        // new singleton view with different project entry
3993        pane.update_in(cx, |pane, window, cx| {
3994            pane.add_item(
3995                Box::new(cx.new(|cx| {
3996                    TestItem::new(cx)
3997                        .with_singleton(true)
3998                        .with_label("buffer 2")
3999                        .with_project_items(&[TestProjectItem::new(2, "2.txt", cx)])
4000                })),
4001                false,
4002                false,
4003                None,
4004                window,
4005                cx,
4006            );
4007        });
4008        assert_item_labels(&pane, ["buffer 1", "buffer 2*"], cx);
4009
4010        // new multibuffer view with the same project entry
4011        pane.update_in(cx, |pane, window, cx| {
4012            pane.add_item(
4013                Box::new(cx.new(|cx| {
4014                    TestItem::new(cx)
4015                        .with_singleton(false)
4016                        .with_label("multibuffer 1")
4017                        .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
4018                })),
4019                false,
4020                false,
4021                None,
4022                window,
4023                cx,
4024            );
4025        });
4026        assert_item_labels(&pane, ["buffer 1", "buffer 2", "multibuffer 1*"], cx);
4027
4028        // another multibuffer view with the same project entry
4029        pane.update_in(cx, |pane, window, cx| {
4030            pane.add_item(
4031                Box::new(cx.new(|cx| {
4032                    TestItem::new(cx)
4033                        .with_singleton(false)
4034                        .with_label("multibuffer 1b")
4035                        .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
4036                })),
4037                false,
4038                false,
4039                None,
4040                window,
4041                cx,
4042            );
4043        });
4044        assert_item_labels(
4045            &pane,
4046            ["buffer 1", "buffer 2", "multibuffer 1", "multibuffer 1b*"],
4047            cx,
4048        );
4049    }
4050
4051    #[gpui::test]
4052    async fn test_remove_item_ordering_history(cx: &mut TestAppContext) {
4053        init_test(cx);
4054        let fs = FakeFs::new(cx.executor());
4055
4056        let project = Project::test(fs, None, cx).await;
4057        let (workspace, cx) =
4058            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4059        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
4060
4061        add_labeled_item(&pane, "A", false, cx);
4062        add_labeled_item(&pane, "B", false, cx);
4063        add_labeled_item(&pane, "C", false, cx);
4064        add_labeled_item(&pane, "D", false, cx);
4065        assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
4066
4067        pane.update_in(cx, |pane, window, cx| {
4068            pane.activate_item(1, false, false, window, cx)
4069        });
4070        add_labeled_item(&pane, "1", false, cx);
4071        assert_item_labels(&pane, ["A", "B", "1*", "C", "D"], cx);
4072
4073        pane.update_in(cx, |pane, window, cx| {
4074            pane.close_active_item(
4075                &CloseActiveItem {
4076                    save_intent: None,
4077                    close_pinned: false,
4078                },
4079                window,
4080                cx,
4081            )
4082        })
4083        .unwrap()
4084        .await
4085        .unwrap();
4086        assert_item_labels(&pane, ["A", "B*", "C", "D"], cx);
4087
4088        pane.update_in(cx, |pane, window, cx| {
4089            pane.activate_item(3, false, false, window, cx)
4090        });
4091        assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
4092
4093        pane.update_in(cx, |pane, window, cx| {
4094            pane.close_active_item(
4095                &CloseActiveItem {
4096                    save_intent: None,
4097                    close_pinned: false,
4098                },
4099                window,
4100                cx,
4101            )
4102        })
4103        .unwrap()
4104        .await
4105        .unwrap();
4106        assert_item_labels(&pane, ["A", "B*", "C"], cx);
4107
4108        pane.update_in(cx, |pane, window, cx| {
4109            pane.close_active_item(
4110                &CloseActiveItem {
4111                    save_intent: None,
4112                    close_pinned: false,
4113                },
4114                window,
4115                cx,
4116            )
4117        })
4118        .unwrap()
4119        .await
4120        .unwrap();
4121        assert_item_labels(&pane, ["A", "C*"], cx);
4122
4123        pane.update_in(cx, |pane, window, cx| {
4124            pane.close_active_item(
4125                &CloseActiveItem {
4126                    save_intent: None,
4127                    close_pinned: false,
4128                },
4129                window,
4130                cx,
4131            )
4132        })
4133        .unwrap()
4134        .await
4135        .unwrap();
4136        assert_item_labels(&pane, ["A*"], cx);
4137    }
4138
4139    #[gpui::test]
4140    async fn test_remove_item_ordering_neighbour(cx: &mut TestAppContext) {
4141        init_test(cx);
4142        cx.update_global::<SettingsStore, ()>(|s, cx| {
4143            s.update_user_settings::<ItemSettings>(cx, |s| {
4144                s.activate_on_close = Some(ActivateOnClose::Neighbour);
4145            });
4146        });
4147        let fs = FakeFs::new(cx.executor());
4148
4149        let project = Project::test(fs, None, cx).await;
4150        let (workspace, cx) =
4151            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4152        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
4153
4154        add_labeled_item(&pane, "A", false, cx);
4155        add_labeled_item(&pane, "B", false, cx);
4156        add_labeled_item(&pane, "C", false, cx);
4157        add_labeled_item(&pane, "D", false, cx);
4158        assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
4159
4160        pane.update_in(cx, |pane, window, cx| {
4161            pane.activate_item(1, false, false, window, cx)
4162        });
4163        add_labeled_item(&pane, "1", false, cx);
4164        assert_item_labels(&pane, ["A", "B", "1*", "C", "D"], cx);
4165
4166        pane.update_in(cx, |pane, window, cx| {
4167            pane.close_active_item(
4168                &CloseActiveItem {
4169                    save_intent: None,
4170                    close_pinned: false,
4171                },
4172                window,
4173                cx,
4174            )
4175        })
4176        .unwrap()
4177        .await
4178        .unwrap();
4179        assert_item_labels(&pane, ["A", "B", "C*", "D"], cx);
4180
4181        pane.update_in(cx, |pane, window, cx| {
4182            pane.activate_item(3, false, false, window, cx)
4183        });
4184        assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
4185
4186        pane.update_in(cx, |pane, window, cx| {
4187            pane.close_active_item(
4188                &CloseActiveItem {
4189                    save_intent: None,
4190                    close_pinned: false,
4191                },
4192                window,
4193                cx,
4194            )
4195        })
4196        .unwrap()
4197        .await
4198        .unwrap();
4199        assert_item_labels(&pane, ["A", "B", "C*"], cx);
4200
4201        pane.update_in(cx, |pane, window, cx| {
4202            pane.close_active_item(
4203                &CloseActiveItem {
4204                    save_intent: None,
4205                    close_pinned: false,
4206                },
4207                window,
4208                cx,
4209            )
4210        })
4211        .unwrap()
4212        .await
4213        .unwrap();
4214        assert_item_labels(&pane, ["A", "B*"], cx);
4215
4216        pane.update_in(cx, |pane, window, cx| {
4217            pane.close_active_item(
4218                &CloseActiveItem {
4219                    save_intent: None,
4220                    close_pinned: false,
4221                },
4222                window,
4223                cx,
4224            )
4225        })
4226        .unwrap()
4227        .await
4228        .unwrap();
4229        assert_item_labels(&pane, ["A*"], cx);
4230    }
4231
4232    #[gpui::test]
4233    async fn test_remove_item_ordering_left_neighbour(cx: &mut TestAppContext) {
4234        init_test(cx);
4235        cx.update_global::<SettingsStore, ()>(|s, cx| {
4236            s.update_user_settings::<ItemSettings>(cx, |s| {
4237                s.activate_on_close = Some(ActivateOnClose::LeftNeighbour);
4238            });
4239        });
4240        let fs = FakeFs::new(cx.executor());
4241
4242        let project = Project::test(fs, None, cx).await;
4243        let (workspace, cx) =
4244            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4245        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
4246
4247        add_labeled_item(&pane, "A", false, cx);
4248        add_labeled_item(&pane, "B", false, cx);
4249        add_labeled_item(&pane, "C", false, cx);
4250        add_labeled_item(&pane, "D", false, cx);
4251        assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
4252
4253        pane.update_in(cx, |pane, window, cx| {
4254            pane.activate_item(1, false, false, window, cx)
4255        });
4256        add_labeled_item(&pane, "1", false, cx);
4257        assert_item_labels(&pane, ["A", "B", "1*", "C", "D"], cx);
4258
4259        pane.update_in(cx, |pane, window, cx| {
4260            pane.close_active_item(
4261                &CloseActiveItem {
4262                    save_intent: None,
4263                    close_pinned: false,
4264                },
4265                window,
4266                cx,
4267            )
4268        })
4269        .unwrap()
4270        .await
4271        .unwrap();
4272        assert_item_labels(&pane, ["A", "B*", "C", "D"], cx);
4273
4274        pane.update_in(cx, |pane, window, cx| {
4275            pane.activate_item(3, false, false, window, cx)
4276        });
4277        assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
4278
4279        pane.update_in(cx, |pane, window, cx| {
4280            pane.close_active_item(
4281                &CloseActiveItem {
4282                    save_intent: None,
4283                    close_pinned: false,
4284                },
4285                window,
4286                cx,
4287            )
4288        })
4289        .unwrap()
4290        .await
4291        .unwrap();
4292        assert_item_labels(&pane, ["A", "B", "C*"], cx);
4293
4294        pane.update_in(cx, |pane, window, cx| {
4295            pane.activate_item(0, false, false, window, cx)
4296        });
4297        assert_item_labels(&pane, ["A*", "B", "C"], cx);
4298
4299        pane.update_in(cx, |pane, window, cx| {
4300            pane.close_active_item(
4301                &CloseActiveItem {
4302                    save_intent: None,
4303                    close_pinned: false,
4304                },
4305                window,
4306                cx,
4307            )
4308        })
4309        .unwrap()
4310        .await
4311        .unwrap();
4312        assert_item_labels(&pane, ["B*", "C"], cx);
4313
4314        pane.update_in(cx, |pane, window, cx| {
4315            pane.close_active_item(
4316                &CloseActiveItem {
4317                    save_intent: None,
4318                    close_pinned: false,
4319                },
4320                window,
4321                cx,
4322            )
4323        })
4324        .unwrap()
4325        .await
4326        .unwrap();
4327        assert_item_labels(&pane, ["C*"], cx);
4328    }
4329
4330    #[gpui::test]
4331    async fn test_close_inactive_items(cx: &mut TestAppContext) {
4332        init_test(cx);
4333        let fs = FakeFs::new(cx.executor());
4334
4335        let project = Project::test(fs, None, cx).await;
4336        let (workspace, cx) =
4337            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4338        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
4339
4340        set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx);
4341
4342        pane.update_in(cx, |pane, window, cx| {
4343            pane.close_inactive_items(
4344                &CloseInactiveItems {
4345                    save_intent: None,
4346                    close_pinned: false,
4347                },
4348                window,
4349                cx,
4350            )
4351        })
4352        .unwrap()
4353        .await
4354        .unwrap();
4355        assert_item_labels(&pane, ["C*"], cx);
4356    }
4357
4358    #[gpui::test]
4359    async fn test_close_clean_items(cx: &mut TestAppContext) {
4360        init_test(cx);
4361        let fs = FakeFs::new(cx.executor());
4362
4363        let project = Project::test(fs, None, cx).await;
4364        let (workspace, cx) =
4365            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4366        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
4367
4368        add_labeled_item(&pane, "A", true, cx);
4369        add_labeled_item(&pane, "B", false, cx);
4370        add_labeled_item(&pane, "C", true, cx);
4371        add_labeled_item(&pane, "D", false, cx);
4372        add_labeled_item(&pane, "E", false, cx);
4373        assert_item_labels(&pane, ["A^", "B", "C^", "D", "E*"], cx);
4374
4375        pane.update_in(cx, |pane, window, cx| {
4376            pane.close_clean_items(
4377                &CloseCleanItems {
4378                    close_pinned: false,
4379                },
4380                window,
4381                cx,
4382            )
4383        })
4384        .unwrap()
4385        .await
4386        .unwrap();
4387        assert_item_labels(&pane, ["A^", "C*^"], cx);
4388    }
4389
4390    #[gpui::test]
4391    async fn test_close_items_to_the_left(cx: &mut TestAppContext) {
4392        init_test(cx);
4393        let fs = FakeFs::new(cx.executor());
4394
4395        let project = Project::test(fs, None, cx).await;
4396        let (workspace, cx) =
4397            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4398        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
4399
4400        set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx);
4401
4402        pane.update_in(cx, |pane, window, cx| {
4403            pane.close_items_to_the_left(
4404                &CloseItemsToTheLeft {
4405                    close_pinned: false,
4406                },
4407                window,
4408                cx,
4409            )
4410        })
4411        .unwrap()
4412        .await
4413        .unwrap();
4414        assert_item_labels(&pane, ["C*", "D", "E"], cx);
4415    }
4416
4417    #[gpui::test]
4418    async fn test_close_items_to_the_right(cx: &mut TestAppContext) {
4419        init_test(cx);
4420        let fs = FakeFs::new(cx.executor());
4421
4422        let project = Project::test(fs, None, cx).await;
4423        let (workspace, cx) =
4424            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4425        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
4426
4427        set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx);
4428
4429        pane.update_in(cx, |pane, window, cx| {
4430            pane.close_items_to_the_right(
4431                &CloseItemsToTheRight {
4432                    close_pinned: false,
4433                },
4434                window,
4435                cx,
4436            )
4437        })
4438        .unwrap()
4439        .await
4440        .unwrap();
4441        assert_item_labels(&pane, ["A", "B", "C*"], cx);
4442    }
4443
4444    #[gpui::test]
4445    async fn test_close_all_items(cx: &mut TestAppContext) {
4446        init_test(cx);
4447        let fs = FakeFs::new(cx.executor());
4448
4449        let project = Project::test(fs, None, cx).await;
4450        let (workspace, cx) =
4451            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4452        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
4453
4454        let item_a = add_labeled_item(&pane, "A", false, cx);
4455        add_labeled_item(&pane, "B", false, cx);
4456        add_labeled_item(&pane, "C", false, cx);
4457        assert_item_labels(&pane, ["A", "B", "C*"], cx);
4458
4459        pane.update_in(cx, |pane, window, cx| {
4460            let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4461            pane.pin_tab_at(ix, window, cx);
4462            pane.close_all_items(
4463                &CloseAllItems {
4464                    save_intent: None,
4465                    close_pinned: false,
4466                },
4467                window,
4468                cx,
4469            )
4470        })
4471        .unwrap()
4472        .await
4473        .unwrap();
4474        assert_item_labels(&pane, ["A*"], cx);
4475
4476        pane.update_in(cx, |pane, window, cx| {
4477            let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4478            pane.unpin_tab_at(ix, window, cx);
4479            pane.close_all_items(
4480                &CloseAllItems {
4481                    save_intent: None,
4482                    close_pinned: false,
4483                },
4484                window,
4485                cx,
4486            )
4487        })
4488        .unwrap()
4489        .await
4490        .unwrap();
4491
4492        assert_item_labels(&pane, [], cx);
4493
4494        add_labeled_item(&pane, "A", true, cx).update(cx, |item, cx| {
4495            item.project_items
4496                .push(TestProjectItem::new_dirty(1, "A.txt", cx))
4497        });
4498        add_labeled_item(&pane, "B", true, cx).update(cx, |item, cx| {
4499            item.project_items
4500                .push(TestProjectItem::new_dirty(2, "B.txt", cx))
4501        });
4502        add_labeled_item(&pane, "C", true, cx).update(cx, |item, cx| {
4503            item.project_items
4504                .push(TestProjectItem::new_dirty(3, "C.txt", cx))
4505        });
4506        assert_item_labels(&pane, ["A^", "B^", "C*^"], cx);
4507
4508        let save = pane
4509            .update_in(cx, |pane, window, cx| {
4510                pane.close_all_items(
4511                    &CloseAllItems {
4512                        save_intent: None,
4513                        close_pinned: false,
4514                    },
4515                    window,
4516                    cx,
4517                )
4518            })
4519            .unwrap();
4520
4521        cx.executor().run_until_parked();
4522        cx.simulate_prompt_answer("Save all");
4523        save.await.unwrap();
4524        assert_item_labels(&pane, [], cx);
4525
4526        add_labeled_item(&pane, "A", true, cx);
4527        add_labeled_item(&pane, "B", true, cx);
4528        add_labeled_item(&pane, "C", true, cx);
4529        assert_item_labels(&pane, ["A^", "B^", "C*^"], cx);
4530        let save = pane
4531            .update_in(cx, |pane, window, cx| {
4532                pane.close_all_items(
4533                    &CloseAllItems {
4534                        save_intent: None,
4535                        close_pinned: false,
4536                    },
4537                    window,
4538                    cx,
4539                )
4540            })
4541            .unwrap();
4542
4543        cx.executor().run_until_parked();
4544        cx.simulate_prompt_answer("Discard all");
4545        save.await.unwrap();
4546        assert_item_labels(&pane, [], cx);
4547    }
4548
4549    #[gpui::test]
4550    async fn test_close_with_save_intent(cx: &mut TestAppContext) {
4551        init_test(cx);
4552        let fs = FakeFs::new(cx.executor());
4553
4554        let project = Project::test(fs, None, cx).await;
4555        let (workspace, cx) =
4556            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
4557        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
4558
4559        let a = cx.update(|_, cx| TestProjectItem::new_dirty(1, "A.txt", cx));
4560        let b = cx.update(|_, cx| TestProjectItem::new_dirty(1, "B.txt", cx));
4561        let c = cx.update(|_, cx| TestProjectItem::new_dirty(1, "C.txt", cx));
4562
4563        add_labeled_item(&pane, "AB", true, cx).update(cx, |item, _| {
4564            item.project_items.push(a.clone());
4565            item.project_items.push(b.clone());
4566        });
4567        add_labeled_item(&pane, "C", true, cx)
4568            .update(cx, |item, _| item.project_items.push(c.clone()));
4569        assert_item_labels(&pane, ["AB^", "C*^"], cx);
4570
4571        pane.update_in(cx, |pane, window, cx| {
4572            pane.close_all_items(
4573                &CloseAllItems {
4574                    save_intent: Some(SaveIntent::Save),
4575                    close_pinned: false,
4576                },
4577                window,
4578                cx,
4579            )
4580        })
4581        .unwrap()
4582        .await
4583        .unwrap();
4584
4585        assert_item_labels(&pane, [], cx);
4586        cx.update(|_, cx| {
4587            assert!(!a.read(cx).is_dirty);
4588            assert!(!b.read(cx).is_dirty);
4589            assert!(!c.read(cx).is_dirty);
4590        });
4591    }
4592
4593    #[gpui::test]
4594    async fn test_close_all_items_including_pinned(cx: &mut TestAppContext) {
4595        init_test(cx);
4596        let fs = FakeFs::new(cx.executor());
4597
4598        let project = Project::test(fs, None, cx).await;
4599        let (workspace, cx) =
4600            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
4601        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
4602
4603        let item_a = add_labeled_item(&pane, "A", false, cx);
4604        add_labeled_item(&pane, "B", false, cx);
4605        add_labeled_item(&pane, "C", false, cx);
4606        assert_item_labels(&pane, ["A", "B", "C*"], cx);
4607
4608        pane.update_in(cx, |pane, window, cx| {
4609            let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4610            pane.pin_tab_at(ix, window, cx);
4611            pane.close_all_items(
4612                &CloseAllItems {
4613                    save_intent: None,
4614                    close_pinned: true,
4615                },
4616                window,
4617                cx,
4618            )
4619        })
4620        .unwrap()
4621        .await
4622        .unwrap();
4623        assert_item_labels(&pane, [], cx);
4624    }
4625
4626    #[gpui::test]
4627    async fn test_close_pinned_tab_with_non_pinned_in_same_pane(cx: &mut TestAppContext) {
4628        init_test(cx);
4629        let fs = FakeFs::new(cx.executor());
4630        let project = Project::test(fs, None, cx).await;
4631        let (workspace, cx) =
4632            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
4633
4634        // Non-pinned tabs in same pane
4635        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
4636        add_labeled_item(&pane, "A", false, cx);
4637        add_labeled_item(&pane, "B", false, cx);
4638        add_labeled_item(&pane, "C", false, cx);
4639        pane.update_in(cx, |pane, window, cx| {
4640            pane.pin_tab_at(0, window, cx);
4641        });
4642        set_labeled_items(&pane, ["A*", "B", "C"], cx);
4643        pane.update_in(cx, |pane, window, cx| {
4644            pane.close_active_item(
4645                &CloseActiveItem {
4646                    save_intent: None,
4647                    close_pinned: false,
4648                },
4649                window,
4650                cx,
4651            );
4652        });
4653        // Non-pinned tab should be active
4654        assert_item_labels(&pane, ["A", "B*", "C"], cx);
4655    }
4656
4657    #[gpui::test]
4658    async fn test_close_pinned_tab_with_non_pinned_in_different_pane(cx: &mut TestAppContext) {
4659        init_test(cx);
4660        let fs = FakeFs::new(cx.executor());
4661        let project = Project::test(fs, None, cx).await;
4662        let (workspace, cx) =
4663            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
4664
4665        // No non-pinned tabs in same pane, non-pinned tabs in another pane
4666        let pane1 = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
4667        let pane2 = workspace.update_in(cx, |workspace, window, cx| {
4668            workspace.split_pane(pane1.clone(), SplitDirection::Right, window, cx)
4669        });
4670        add_labeled_item(&pane1, "A", false, cx);
4671        pane1.update_in(cx, |pane, window, cx| {
4672            pane.pin_tab_at(0, window, cx);
4673        });
4674        set_labeled_items(&pane1, ["A*"], cx);
4675        add_labeled_item(&pane2, "B", false, cx);
4676        set_labeled_items(&pane2, ["B"], cx);
4677        pane1.update_in(cx, |pane, window, cx| {
4678            pane.close_active_item(
4679                &CloseActiveItem {
4680                    save_intent: None,
4681                    close_pinned: false,
4682                },
4683                window,
4684                cx,
4685            );
4686        });
4687        //  Non-pinned tab of other pane should be active
4688        assert_item_labels(&pane2, ["B*"], cx);
4689    }
4690
4691    fn init_test(cx: &mut TestAppContext) {
4692        cx.update(|cx| {
4693            let settings_store = SettingsStore::test(cx);
4694            cx.set_global(settings_store);
4695            theme::init(LoadThemes::JustBase, cx);
4696            crate::init_settings(cx);
4697            Project::init_settings(cx);
4698        });
4699    }
4700
4701    fn set_max_tabs(cx: &mut TestAppContext, value: Option<usize>) {
4702        cx.update_global(|store: &mut SettingsStore, cx| {
4703            store.update_user_settings::<WorkspaceSettings>(cx, |settings| {
4704                settings.max_tabs = value.map(|v| NonZero::new(v).unwrap())
4705            });
4706        });
4707    }
4708
4709    fn add_labeled_item(
4710        pane: &Entity<Pane>,
4711        label: &str,
4712        is_dirty: bool,
4713        cx: &mut VisualTestContext,
4714    ) -> Box<Entity<TestItem>> {
4715        pane.update_in(cx, |pane, window, cx| {
4716            let labeled_item =
4717                Box::new(cx.new(|cx| TestItem::new(cx).with_label(label).with_dirty(is_dirty)));
4718            pane.add_item(labeled_item.clone(), false, false, None, window, cx);
4719            labeled_item
4720        })
4721    }
4722
4723    fn set_labeled_items<const COUNT: usize>(
4724        pane: &Entity<Pane>,
4725        labels: [&str; COUNT],
4726        cx: &mut VisualTestContext,
4727    ) -> [Box<Entity<TestItem>>; COUNT] {
4728        pane.update_in(cx, |pane, window, cx| {
4729            pane.items.clear();
4730            let mut active_item_index = 0;
4731
4732            let mut index = 0;
4733            let items = labels.map(|mut label| {
4734                if label.ends_with('*') {
4735                    label = label.trim_end_matches('*');
4736                    active_item_index = index;
4737                }
4738
4739                let labeled_item = Box::new(cx.new(|cx| TestItem::new(cx).with_label(label)));
4740                pane.add_item(labeled_item.clone(), false, false, None, window, cx);
4741                index += 1;
4742                labeled_item
4743            });
4744
4745            pane.activate_item(active_item_index, false, false, window, cx);
4746
4747            items
4748        })
4749    }
4750
4751    // Assert the item label, with the active item label suffixed with a '*'
4752    #[track_caller]
4753    fn assert_item_labels<const COUNT: usize>(
4754        pane: &Entity<Pane>,
4755        expected_states: [&str; COUNT],
4756        cx: &mut VisualTestContext,
4757    ) {
4758        let actual_states = pane.update(cx, |pane, cx| {
4759            pane.items
4760                .iter()
4761                .enumerate()
4762                .map(|(ix, item)| {
4763                    let mut state = item
4764                        .to_any()
4765                        .downcast::<TestItem>()
4766                        .unwrap()
4767                        .read(cx)
4768                        .label
4769                        .clone();
4770                    if ix == pane.active_item_index {
4771                        state.push('*');
4772                    }
4773                    if item.is_dirty(cx) {
4774                        state.push('^');
4775                    }
4776                    state
4777                })
4778                .collect::<Vec<_>>()
4779        });
4780        assert_eq!(
4781            actual_states, expected_states,
4782            "pane items do not match expectation"
4783        );
4784    }
4785}