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