pane.rs

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