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