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