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