pane.rs

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