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