pane.rs

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