pane.rs

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