pane.rs

   1use crate::{
   2    CloseWindow, NewFile, NewTerminal, OpenInTerminal, OpenOptions, OpenTerminal, OpenVisible,
   3    SplitDirection, ToggleFileFinder, ToggleProjectSymbols, ToggleZoom, Workspace,
   4    WorkspaceItemBuilder,
   5    item::{
   6        ActivateOnClose, ClosePosition, Item, ItemHandle, ItemSettings, PreviewTabsSettings,
   7        ProjectItemKind, ShowCloseButton, ShowDiagnostics, TabContentParams, TabTooltipContent,
   8        WeakItemHandle,
   9    },
  10    move_item,
  11    notifications::NotifyResultExt,
  12    toolbar::Toolbar,
  13    workspace_settings::{AutosaveSetting, TabBarSettings, WorkspaceSettings},
  14};
  15use anyhow::Result;
  16use collections::{BTreeSet, HashMap, HashSet, VecDeque};
  17use futures::{StreamExt, stream::FuturesUnordered};
  18use gpui::{
  19    Action, AnyElement, App, AsyncWindowContext, ClickEvent, ClipboardItem, Context, Corner, Div,
  20    DragMoveEvent, Entity, EntityId, EventEmitter, ExternalPaths, FocusHandle, FocusOutEvent,
  21    Focusable, KeyContext, MouseButton, MouseDownEvent, NavigationDirection, Pixels, Point,
  22    PromptLevel, Render, ScrollHandle, Subscription, Task, WeakEntity, WeakFocusHandle, Window,
  23    actions, anchored, deferred, impl_actions, prelude::*,
  24};
  25use itertools::Itertools;
  26use language::DiagnosticSeverity;
  27use parking_lot::Mutex;
  28use project::{Project, ProjectEntryId, ProjectPath, WorktreeId};
  29use schemars::JsonSchema;
  30use serde::Deserialize;
  31use settings::{Settings, SettingsStore};
  32use std::{
  33    any::Any,
  34    cmp, fmt, mem,
  35    ops::ControlFlow,
  36    path::PathBuf,
  37    rc::Rc,
  38    sync::{
  39        Arc,
  40        atomic::{AtomicUsize, Ordering},
  41    },
  42};
  43use theme::ThemeSettings;
  44use ui::{
  45    ButtonSize, Color, ContextMenu, ContextMenuEntry, ContextMenuItem, DecoratedIcon, IconButton,
  46    IconButtonShape, IconDecoration, IconDecorationKind, IconName, IconSize, Indicator, Label,
  47    PopoverMenu, PopoverMenuHandle, ScrollableHandle, Tab, TabBar, TabPosition, Tooltip,
  48    prelude::*, right_click_menu,
  49};
  50use util::{ResultExt, debug_panic, maybe, truncate_and_remove_front};
  51
  52/// A selected entry in e.g. project panel.
  53#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
  54pub struct SelectedEntry {
  55    pub worktree_id: WorktreeId,
  56    pub entry_id: ProjectEntryId,
  57}
  58
  59/// A group of selected entries from project panel.
  60#[derive(Debug)]
  61pub struct DraggedSelection {
  62    pub active_selection: SelectedEntry,
  63    pub marked_selections: Arc<BTreeSet<SelectedEntry>>,
  64}
  65
  66impl DraggedSelection {
  67    pub fn items<'a>(&'a self) -> Box<dyn Iterator<Item = &'a SelectedEntry> + 'a> {
  68        if self.marked_selections.contains(&self.active_selection) {
  69            Box::new(self.marked_selections.iter())
  70        } else {
  71            Box::new(std::iter::once(&self.active_selection))
  72        }
  73    }
  74}
  75
  76#[derive(Clone, Copy, PartialEq, Debug, Deserialize, JsonSchema)]
  77#[serde(rename_all = "snake_case")]
  78pub enum SaveIntent {
  79    /// write all files (even if unchanged)
  80    /// prompt before overwriting on-disk changes
  81    Save,
  82    /// same as Save, but without auto formatting
  83    SaveWithoutFormat,
  84    /// write any files that have local changes
  85    /// prompt before overwriting on-disk changes
  86    SaveAll,
  87    /// always prompt for a new path
  88    SaveAs,
  89    /// prompt "you have unsaved changes" before writing
  90    Close,
  91    /// write all dirty files, don't prompt on conflict
  92    Overwrite,
  93    /// skip all save-related behavior
  94    Skip,
  95}
  96
  97#[derive(Clone, PartialEq, Debug, Deserialize, JsonSchema, Default)]
  98pub struct ActivateItem(pub usize);
  99
 100#[derive(Clone, PartialEq, Debug, Deserialize, JsonSchema, Default)]
 101#[serde(deny_unknown_fields)]
 102pub struct CloseActiveItem {
 103    pub save_intent: Option<SaveIntent>,
 104    #[serde(default)]
 105    pub close_pinned: bool,
 106}
 107
 108#[derive(Clone, PartialEq, Debug, Deserialize, JsonSchema, Default)]
 109#[serde(deny_unknown_fields)]
 110pub struct CloseInactiveItems {
 111    pub save_intent: Option<SaveIntent>,
 112    #[serde(default)]
 113    pub close_pinned: bool,
 114}
 115
 116#[derive(Clone, PartialEq, Debug, Deserialize, JsonSchema, Default)]
 117#[serde(deny_unknown_fields)]
 118pub struct CloseAllItems {
 119    pub save_intent: Option<SaveIntent>,
 120    #[serde(default)]
 121    pub close_pinned: bool,
 122}
 123
 124#[derive(Clone, PartialEq, Debug, Deserialize, JsonSchema, Default)]
 125#[serde(deny_unknown_fields)]
 126pub struct CloseCleanItems {
 127    #[serde(default)]
 128    pub close_pinned: bool,
 129}
 130
 131#[derive(Clone, PartialEq, Debug, Deserialize, JsonSchema, Default)]
 132#[serde(deny_unknown_fields)]
 133pub struct CloseItemsToTheRight {
 134    #[serde(default)]
 135    pub close_pinned: bool,
 136}
 137
 138#[derive(Clone, PartialEq, Debug, Deserialize, JsonSchema, Default)]
 139#[serde(deny_unknown_fields)]
 140pub struct CloseItemsToTheLeft {
 141    #[serde(default)]
 142    pub close_pinned: bool,
 143}
 144
 145#[derive(Clone, PartialEq, Debug, Deserialize, JsonSchema, Default)]
 146#[serde(deny_unknown_fields)]
 147pub struct RevealInProjectPanel {
 148    #[serde(skip)]
 149    pub entry_id: Option<u64>,
 150}
 151
 152#[derive(Clone, PartialEq, Debug, Deserialize, JsonSchema, Default)]
 153#[serde(deny_unknown_fields)]
 154pub struct DeploySearch {
 155    #[serde(default)]
 156    pub replace_enabled: bool,
 157    #[serde(default)]
 158    pub included_files: Option<String>,
 159}
 160
 161impl_actions!(
 162    pane,
 163    [
 164        CloseAllItems,
 165        CloseActiveItem,
 166        CloseCleanItems,
 167        CloseItemsToTheLeft,
 168        CloseItemsToTheRight,
 169        CloseInactiveItems,
 170        ActivateItem,
 171        RevealInProjectPanel,
 172        DeploySearch,
 173    ]
 174);
 175
 176actions!(
 177    pane,
 178    [
 179        ActivatePreviousItem,
 180        ActivateNextItem,
 181        ActivateLastItem,
 182        AlternateFile,
 183        GoBack,
 184        GoForward,
 185        JoinIntoNext,
 186        JoinAll,
 187        ReopenClosedItem,
 188        SplitLeft,
 189        SplitUp,
 190        SplitRight,
 191        SplitDown,
 192        SplitHorizontal,
 193        SplitVertical,
 194        SwapItemLeft,
 195        SwapItemRight,
 196        TogglePreviewTab,
 197        TogglePinTab,
 198    ]
 199);
 200
 201impl DeploySearch {
 202    pub fn find() -> Self {
 203        Self {
 204            replace_enabled: false,
 205            included_files: None,
 206        }
 207    }
 208}
 209
 210const MAX_NAVIGATION_HISTORY_LEN: usize = 1024;
 211
 212pub enum Event {
 213    AddItem {
 214        item: Box<dyn ItemHandle>,
 215    },
 216    ActivateItem {
 217        local: bool,
 218        focus_changed: bool,
 219    },
 220    Remove {
 221        focus_on_pane: Option<Entity<Pane>>,
 222    },
 223    RemoveItem {
 224        idx: usize,
 225    },
 226    RemovedItem {
 227        item: Box<dyn ItemHandle>,
 228    },
 229    Split(SplitDirection),
 230    JoinAll,
 231    JoinIntoNext,
 232    ChangeItemTitle,
 233    Focus,
 234    ZoomIn,
 235    ZoomOut,
 236    UserSavedItem {
 237        item: Box<dyn WeakItemHandle>,
 238        save_intent: SaveIntent,
 239    },
 240}
 241
 242impl fmt::Debug for Event {
 243    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
 244        match self {
 245            Event::AddItem { item } => f
 246                .debug_struct("AddItem")
 247                .field("item", &item.item_id())
 248                .finish(),
 249            Event::ActivateItem { local, .. } => f
 250                .debug_struct("ActivateItem")
 251                .field("local", local)
 252                .finish(),
 253            Event::Remove { .. } => f.write_str("Remove"),
 254            Event::RemoveItem { idx } => f.debug_struct("RemoveItem").field("idx", idx).finish(),
 255            Event::RemovedItem { item } => f
 256                .debug_struct("RemovedItem")
 257                .field("item", &item.item_id())
 258                .finish(),
 259            Event::Split(direction) => f
 260                .debug_struct("Split")
 261                .field("direction", direction)
 262                .finish(),
 263            Event::JoinAll => f.write_str("JoinAll"),
 264            Event::JoinIntoNext => f.write_str("JoinIntoNext"),
 265            Event::ChangeItemTitle => f.write_str("ChangeItemTitle"),
 266            Event::Focus => f.write_str("Focus"),
 267            Event::ZoomIn => f.write_str("ZoomIn"),
 268            Event::ZoomOut => f.write_str("ZoomOut"),
 269            Event::UserSavedItem { item, save_intent } => f
 270                .debug_struct("UserSavedItem")
 271                .field("item", &item.id())
 272                .field("save_intent", save_intent)
 273                .finish(),
 274        }
 275    }
 276}
 277
 278/// A container for 0 to many items that are open in the workspace.
 279/// Treats all items uniformly via the [`ItemHandle`] trait, whether it's an editor, search results multibuffer, terminal or something else,
 280/// responsible for managing item tabs, focus and zoom states and drag and drop features.
 281/// Can be split, see `PaneGroup` for more details.
 282pub struct Pane {
 283    alternate_file_items: (
 284        Option<Box<dyn WeakItemHandle>>,
 285        Option<Box<dyn WeakItemHandle>>,
 286    ),
 287    focus_handle: FocusHandle,
 288    items: Vec<Box<dyn ItemHandle>>,
 289    activation_history: Vec<ActivationHistoryEntry>,
 290    next_activation_timestamp: Arc<AtomicUsize>,
 291    zoomed: bool,
 292    was_focused: bool,
 293    active_item_index: usize,
 294    preview_item_id: Option<EntityId>,
 295    last_focus_handle_by_item: HashMap<EntityId, WeakFocusHandle>,
 296    nav_history: NavHistory,
 297    toolbar: Entity<Toolbar>,
 298    pub(crate) workspace: WeakEntity<Workspace>,
 299    project: WeakEntity<Project>,
 300    pub drag_split_direction: Option<SplitDirection>,
 301    can_drop_predicate: Option<Arc<dyn Fn(&dyn Any, &mut Window, &mut App) -> bool>>,
 302    custom_drop_handle: Option<
 303        Arc<dyn Fn(&mut Pane, &dyn Any, &mut Window, &mut Context<Pane>) -> ControlFlow<(), ()>>,
 304    >,
 305    can_split_predicate:
 306        Option<Arc<dyn Fn(&mut Self, &dyn Any, &mut Window, &mut Context<Self>) -> bool>>,
 307    should_display_tab_bar: Rc<dyn Fn(&Window, &mut Context<Pane>) -> bool>,
 308    render_tab_bar_buttons: Rc<
 309        dyn Fn(
 310            &mut Pane,
 311            &mut Window,
 312            &mut Context<Pane>,
 313        ) -> (Option<AnyElement>, Option<AnyElement>),
 314    >,
 315    render_tab_bar: Rc<dyn Fn(&mut Pane, &mut Window, &mut Context<Pane>) -> AnyElement>,
 316    show_tab_bar_buttons: bool,
 317    _subscriptions: Vec<Subscription>,
 318    tab_bar_scroll_handle: ScrollHandle,
 319    /// Is None if navigation buttons are permanently turned off (and should not react to setting changes).
 320    /// Otherwise, when `display_nav_history_buttons` is Some, it determines whether nav buttons should be displayed.
 321    display_nav_history_buttons: Option<bool>,
 322    double_click_dispatch_action: Box<dyn Action>,
 323    save_modals_spawned: HashSet<EntityId>,
 324    close_pane_if_empty: bool,
 325    pub new_item_context_menu_handle: PopoverMenuHandle<ContextMenu>,
 326    pub split_item_context_menu_handle: PopoverMenuHandle<ContextMenu>,
 327    pinned_tab_count: usize,
 328    diagnostics: HashMap<ProjectPath, DiagnosticSeverity>,
 329    zoom_out_on_close: bool,
 330    /// If a certain project item wants to get recreated with specific data, it can persist its data before the recreation here.
 331    pub project_item_restoration_data: HashMap<ProjectItemKind, Box<dyn Any + Send>>,
 332}
 333
 334pub struct ActivationHistoryEntry {
 335    pub entity_id: EntityId,
 336    pub timestamp: usize,
 337}
 338
 339pub struct ItemNavHistory {
 340    history: NavHistory,
 341    item: Arc<dyn WeakItemHandle>,
 342    is_preview: bool,
 343}
 344
 345#[derive(Clone)]
 346pub struct NavHistory(Arc<Mutex<NavHistoryState>>);
 347
 348struct NavHistoryState {
 349    mode: NavigationMode,
 350    backward_stack: VecDeque<NavigationEntry>,
 351    forward_stack: VecDeque<NavigationEntry>,
 352    closed_stack: VecDeque<NavigationEntry>,
 353    paths_by_item: HashMap<EntityId, (ProjectPath, Option<PathBuf>)>,
 354    pane: WeakEntity<Pane>,
 355    next_timestamp: Arc<AtomicUsize>,
 356}
 357
 358#[derive(Debug, Copy, Clone)]
 359pub enum NavigationMode {
 360    Normal,
 361    GoingBack,
 362    GoingForward,
 363    ClosingItem,
 364    ReopeningClosedItem,
 365    Disabled,
 366}
 367
 368impl Default for NavigationMode {
 369    fn default() -> Self {
 370        Self::Normal
 371    }
 372}
 373
 374pub struct NavigationEntry {
 375    pub item: Arc<dyn WeakItemHandle>,
 376    pub data: Option<Box<dyn Any + Send>>,
 377    pub timestamp: usize,
 378    pub is_preview: bool,
 379}
 380
 381#[derive(Clone)]
 382pub struct DraggedTab {
 383    pub pane: Entity<Pane>,
 384    pub item: Box<dyn ItemHandle>,
 385    pub ix: usize,
 386    pub detail: usize,
 387    pub is_active: bool,
 388}
 389
 390impl EventEmitter<Event> for Pane {}
 391
 392impl Pane {
 393    pub fn new(
 394        workspace: WeakEntity<Workspace>,
 395        project: Entity<Project>,
 396        next_timestamp: Arc<AtomicUsize>,
 397        can_drop_predicate: Option<Arc<dyn Fn(&dyn Any, &mut Window, &mut App) -> bool + 'static>>,
 398        double_click_dispatch_action: Box<dyn Action>,
 399        window: &mut Window,
 400        cx: &mut Context<Self>,
 401    ) -> Self {
 402        let focus_handle = cx.focus_handle();
 403
 404        let subscriptions = vec![
 405            cx.on_focus(&focus_handle, window, Pane::focus_in),
 406            cx.on_focus_in(&focus_handle, window, Pane::focus_in),
 407            cx.on_focus_out(&focus_handle, window, Pane::focus_out),
 408            cx.observe_global::<SettingsStore>(Self::settings_changed),
 409            cx.subscribe(&project, Self::project_events),
 410        ];
 411
 412        let handle = cx.entity().downgrade();
 413        Self {
 414            alternate_file_items: (None, None),
 415            focus_handle,
 416            items: Vec::new(),
 417            activation_history: Vec::new(),
 418            next_activation_timestamp: next_timestamp.clone(),
 419            was_focused: false,
 420            zoomed: false,
 421            active_item_index: 0,
 422            preview_item_id: None,
 423            last_focus_handle_by_item: Default::default(),
 424            nav_history: NavHistory(Arc::new(Mutex::new(NavHistoryState {
 425                mode: NavigationMode::Normal,
 426                backward_stack: Default::default(),
 427                forward_stack: Default::default(),
 428                closed_stack: Default::default(),
 429                paths_by_item: Default::default(),
 430                pane: handle.clone(),
 431                next_timestamp,
 432            }))),
 433            toolbar: cx.new(|_| Toolbar::new()),
 434            tab_bar_scroll_handle: ScrollHandle::new(),
 435            drag_split_direction: None,
 436            workspace,
 437            project: project.downgrade(),
 438            can_drop_predicate,
 439            custom_drop_handle: None,
 440            can_split_predicate: None,
 441            should_display_tab_bar: Rc::new(|_, cx| TabBarSettings::get_global(cx).show),
 442            render_tab_bar_buttons: Rc::new(default_render_tab_bar_buttons),
 443            render_tab_bar: Rc::new(Self::render_tab_bar),
 444            show_tab_bar_buttons: TabBarSettings::get_global(cx).show_tab_bar_buttons,
 445            display_nav_history_buttons: Some(
 446                TabBarSettings::get_global(cx).show_nav_history_buttons,
 447            ),
 448            _subscriptions: subscriptions,
 449            double_click_dispatch_action,
 450            save_modals_spawned: HashSet::default(),
 451            close_pane_if_empty: true,
 452            split_item_context_menu_handle: Default::default(),
 453            new_item_context_menu_handle: Default::default(),
 454            pinned_tab_count: 0,
 455            diagnostics: Default::default(),
 456            zoom_out_on_close: true,
 457            project_item_restoration_data: HashMap::default(),
 458        }
 459    }
 460
 461    fn alternate_file(&mut self, window: &mut Window, cx: &mut Context<Pane>) {
 462        let (_, alternative) = &self.alternate_file_items;
 463        if let Some(alternative) = alternative {
 464            let existing = self
 465                .items()
 466                .find_position(|item| item.item_id() == alternative.id());
 467            if let Some((ix, _)) = existing {
 468                self.activate_item(ix, true, true, window, cx);
 469            } else if let Some(upgraded) = alternative.upgrade() {
 470                self.add_item(upgraded, true, true, None, window, cx);
 471            }
 472        }
 473    }
 474
 475    pub fn track_alternate_file_items(&mut self) {
 476        if let Some(item) = self.active_item().map(|item| item.downgrade_item()) {
 477            let (current, _) = &self.alternate_file_items;
 478            match current {
 479                Some(current) => {
 480                    if current.id() != item.id() {
 481                        self.alternate_file_items =
 482                            (Some(item), self.alternate_file_items.0.take());
 483                    }
 484                }
 485                None => {
 486                    self.alternate_file_items = (Some(item), None);
 487                }
 488            }
 489        }
 490    }
 491
 492    pub fn has_focus(&self, window: &Window, cx: &App) -> bool {
 493        // We not only check whether our focus handle contains focus, but also
 494        // whether the active item might have focus, because we might have just activated an item
 495        // that hasn't rendered yet.
 496        // Before the next render, we might transfer focus
 497        // to the item, and `focus_handle.contains_focus` returns false because the `active_item`
 498        // is not hooked up to us in the dispatch tree.
 499        self.focus_handle.contains_focused(window, cx)
 500            || self.active_item().map_or(false, |item| {
 501                item.item_focus_handle(cx).contains_focused(window, cx)
 502            })
 503    }
 504
 505    fn focus_in(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 506        if !self.was_focused {
 507            self.was_focused = true;
 508            self.update_history(self.active_item_index);
 509            cx.emit(Event::Focus);
 510            cx.notify();
 511        }
 512
 513        self.toolbar.update(cx, |toolbar, cx| {
 514            toolbar.focus_changed(true, window, cx);
 515        });
 516
 517        if let Some(active_item) = self.active_item() {
 518            if self.focus_handle.is_focused(window) {
 519                // Schedule a redraw next frame, so that the focus changes below take effect
 520                cx.on_next_frame(window, |_, _, cx| {
 521                    cx.notify();
 522                });
 523
 524                // Pane was focused directly. We need to either focus a view inside the active item,
 525                // or focus the active item itself
 526                if let Some(weak_last_focus_handle) =
 527                    self.last_focus_handle_by_item.get(&active_item.item_id())
 528                {
 529                    if let Some(focus_handle) = weak_last_focus_handle.upgrade() {
 530                        focus_handle.focus(window);
 531                        return;
 532                    }
 533                }
 534
 535                active_item.item_focus_handle(cx).focus(window);
 536            } else if let Some(focused) = window.focused(cx) {
 537                if !self.context_menu_focused(window, cx) {
 538                    self.last_focus_handle_by_item
 539                        .insert(active_item.item_id(), focused.downgrade());
 540                }
 541            }
 542        }
 543    }
 544
 545    pub fn context_menu_focused(&self, window: &mut Window, cx: &mut Context<Self>) -> bool {
 546        self.new_item_context_menu_handle.is_focused(window, cx)
 547            || self.split_item_context_menu_handle.is_focused(window, cx)
 548    }
 549
 550    fn focus_out(&mut self, _event: FocusOutEvent, window: &mut Window, cx: &mut Context<Self>) {
 551        self.was_focused = false;
 552        self.toolbar.update(cx, |toolbar, cx| {
 553            toolbar.focus_changed(false, window, cx);
 554        });
 555        cx.notify();
 556    }
 557
 558    fn project_events(
 559        &mut self,
 560        _project: Entity<Project>,
 561        event: &project::Event,
 562        cx: &mut Context<Self>,
 563    ) {
 564        match event {
 565            project::Event::DiskBasedDiagnosticsFinished { .. }
 566            | project::Event::DiagnosticsUpdated { .. } => {
 567                if ItemSettings::get_global(cx).show_diagnostics != ShowDiagnostics::Off {
 568                    self.update_diagnostics(cx);
 569                    cx.notify();
 570                }
 571            }
 572            _ => {}
 573        }
 574    }
 575
 576    fn update_diagnostics(&mut self, cx: &mut Context<Self>) {
 577        let Some(project) = self.project.upgrade() else {
 578            return;
 579        };
 580        let show_diagnostics = ItemSettings::get_global(cx).show_diagnostics;
 581        self.diagnostics = if show_diagnostics != ShowDiagnostics::Off {
 582            project
 583                .read(cx)
 584                .diagnostic_summaries(false, cx)
 585                .filter_map(|(project_path, _, diagnostic_summary)| {
 586                    if diagnostic_summary.error_count > 0 {
 587                        Some((project_path, DiagnosticSeverity::ERROR))
 588                    } else if diagnostic_summary.warning_count > 0
 589                        && show_diagnostics != ShowDiagnostics::Errors
 590                    {
 591                        Some((project_path, DiagnosticSeverity::WARNING))
 592                    } else {
 593                        None
 594                    }
 595                })
 596                .collect()
 597        } else {
 598            HashMap::default()
 599        }
 600    }
 601
 602    fn settings_changed(&mut self, cx: &mut Context<Self>) {
 603        let tab_bar_settings = TabBarSettings::get_global(cx);
 604
 605        if let Some(display_nav_history_buttons) = self.display_nav_history_buttons.as_mut() {
 606            *display_nav_history_buttons = tab_bar_settings.show_nav_history_buttons;
 607        }
 608        self.show_tab_bar_buttons = tab_bar_settings.show_tab_bar_buttons;
 609
 610        if !PreviewTabsSettings::get_global(cx).enabled {
 611            self.preview_item_id = None;
 612        }
 613        self.update_diagnostics(cx);
 614        cx.notify();
 615    }
 616
 617    pub fn active_item_index(&self) -> usize {
 618        self.active_item_index
 619    }
 620
 621    pub fn activation_history(&self) -> &[ActivationHistoryEntry] {
 622        &self.activation_history
 623    }
 624
 625    pub fn set_should_display_tab_bar<F>(&mut self, should_display_tab_bar: F)
 626    where
 627        F: 'static + Fn(&Window, &mut Context<Pane>) -> bool,
 628    {
 629        self.should_display_tab_bar = Rc::new(should_display_tab_bar);
 630    }
 631
 632    pub fn set_can_split(
 633        &mut self,
 634        can_split_predicate: Option<
 635            Arc<dyn Fn(&mut Self, &dyn Any, &mut Window, &mut Context<Self>) -> bool + 'static>,
 636        >,
 637    ) {
 638        self.can_split_predicate = can_split_predicate;
 639    }
 640
 641    pub fn set_close_pane_if_empty(&mut self, close_pane_if_empty: bool, cx: &mut Context<Self>) {
 642        self.close_pane_if_empty = close_pane_if_empty;
 643        cx.notify();
 644    }
 645
 646    pub fn set_can_navigate(&mut self, can_navigate: bool, cx: &mut Context<Self>) {
 647        self.toolbar.update(cx, |toolbar, cx| {
 648            toolbar.set_can_navigate(can_navigate, cx);
 649        });
 650        cx.notify();
 651    }
 652
 653    pub fn set_render_tab_bar<F>(&mut self, cx: &mut Context<Self>, render: F)
 654    where
 655        F: 'static + Fn(&mut Pane, &mut Window, &mut Context<Pane>) -> AnyElement,
 656    {
 657        self.render_tab_bar = Rc::new(render);
 658        cx.notify();
 659    }
 660
 661    pub fn set_render_tab_bar_buttons<F>(&mut self, cx: &mut Context<Self>, render: F)
 662    where
 663        F: 'static
 664            + Fn(
 665                &mut Pane,
 666                &mut Window,
 667                &mut Context<Pane>,
 668            ) -> (Option<AnyElement>, Option<AnyElement>),
 669    {
 670        self.render_tab_bar_buttons = Rc::new(render);
 671        cx.notify();
 672    }
 673
 674    pub fn set_custom_drop_handle<F>(&mut self, cx: &mut Context<Self>, handle: F)
 675    where
 676        F: 'static
 677            + Fn(&mut Pane, &dyn Any, &mut Window, &mut Context<Pane>) -> ControlFlow<(), ()>,
 678    {
 679        self.custom_drop_handle = Some(Arc::new(handle));
 680        cx.notify();
 681    }
 682
 683    pub fn nav_history_for_item<T: Item>(&self, item: &Entity<T>) -> ItemNavHistory {
 684        ItemNavHistory {
 685            history: self.nav_history.clone(),
 686            item: Arc::new(item.downgrade()),
 687            is_preview: self.preview_item_id == Some(item.item_id()),
 688        }
 689    }
 690
 691    pub fn nav_history(&self) -> &NavHistory {
 692        &self.nav_history
 693    }
 694
 695    pub fn nav_history_mut(&mut self) -> &mut NavHistory {
 696        &mut self.nav_history
 697    }
 698
 699    pub fn disable_history(&mut self) {
 700        self.nav_history.disable();
 701    }
 702
 703    pub fn enable_history(&mut self) {
 704        self.nav_history.enable();
 705    }
 706
 707    pub fn can_navigate_backward(&self) -> bool {
 708        !self.nav_history.0.lock().backward_stack.is_empty()
 709    }
 710
 711    pub fn can_navigate_forward(&self) -> bool {
 712        !self.nav_history.0.lock().forward_stack.is_empty()
 713    }
 714
 715    fn navigate_backward(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 716        if let Some(workspace) = self.workspace.upgrade() {
 717            let pane = cx.entity().downgrade();
 718            window.defer(cx, move |window, cx| {
 719                workspace.update(cx, |workspace, cx| {
 720                    workspace.go_back(pane, window, cx).detach_and_log_err(cx)
 721                })
 722            })
 723        }
 724    }
 725
 726    fn navigate_forward(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 727        if let Some(workspace) = self.workspace.upgrade() {
 728            let pane = cx.entity().downgrade();
 729            window.defer(cx, move |window, cx| {
 730                workspace.update(cx, |workspace, cx| {
 731                    workspace
 732                        .go_forward(pane, window, cx)
 733                        .detach_and_log_err(cx)
 734                })
 735            })
 736        }
 737    }
 738
 739    fn history_updated(&mut self, cx: &mut Context<Self>) {
 740        self.toolbar.update(cx, |_, cx| cx.notify());
 741    }
 742
 743    pub fn preview_item_id(&self) -> Option<EntityId> {
 744        self.preview_item_id
 745    }
 746
 747    pub fn preview_item(&self) -> Option<Box<dyn ItemHandle>> {
 748        self.preview_item_id
 749            .and_then(|id| self.items.iter().find(|item| item.item_id() == id))
 750            .cloned()
 751    }
 752
 753    pub fn preview_item_idx(&self) -> Option<usize> {
 754        if let Some(preview_item_id) = self.preview_item_id {
 755            self.items
 756                .iter()
 757                .position(|item| item.item_id() == preview_item_id)
 758        } else {
 759            None
 760        }
 761    }
 762
 763    pub fn is_active_preview_item(&self, item_id: EntityId) -> bool {
 764        self.preview_item_id == Some(item_id)
 765    }
 766
 767    /// Marks the item with the given ID as the preview item.
 768    /// This will be ignored if the global setting `preview_tabs` is disabled.
 769    pub fn set_preview_item_id(&mut self, item_id: Option<EntityId>, cx: &App) {
 770        if PreviewTabsSettings::get_global(cx).enabled {
 771            self.preview_item_id = item_id;
 772        }
 773    }
 774
 775    pub(crate) fn set_pinned_count(&mut self, count: usize) {
 776        self.pinned_tab_count = count;
 777    }
 778
 779    pub(crate) fn pinned_count(&self) -> usize {
 780        self.pinned_tab_count
 781    }
 782
 783    pub fn handle_item_edit(&mut self, item_id: EntityId, cx: &App) {
 784        if let Some(preview_item) = self.preview_item() {
 785            if preview_item.item_id() == item_id && !preview_item.preserve_preview(cx) {
 786                self.set_preview_item_id(None, cx);
 787            }
 788        }
 789    }
 790
 791    pub(crate) fn open_item(
 792        &mut self,
 793        project_entry_id: Option<ProjectEntryId>,
 794        focus_item: bool,
 795        allow_preview: bool,
 796        activate: bool,
 797        suggested_position: Option<usize>,
 798        window: &mut Window,
 799        cx: &mut Context<Self>,
 800        build_item: WorkspaceItemBuilder,
 801    ) -> Box<dyn ItemHandle> {
 802        let mut existing_item = None;
 803        if let Some(project_entry_id) = project_entry_id {
 804            for (index, item) in self.items.iter().enumerate() {
 805                if item.is_singleton(cx)
 806                    && item.project_entry_ids(cx).as_slice() == [project_entry_id]
 807                {
 808                    let item = item.boxed_clone();
 809                    existing_item = Some((index, item));
 810                    break;
 811                }
 812            }
 813        }
 814        if let Some((index, existing_item)) = existing_item {
 815            // If the item is already open, and the item is a preview item
 816            // and we are not allowing items to open as preview, mark the item as persistent.
 817            if let Some(preview_item_id) = self.preview_item_id {
 818                if let Some(tab) = self.items.get(index) {
 819                    if tab.item_id() == preview_item_id && !allow_preview {
 820                        self.set_preview_item_id(None, cx);
 821                    }
 822                }
 823            }
 824            if activate {
 825                self.activate_item(index, focus_item, focus_item, window, cx);
 826            }
 827            existing_item
 828        } else {
 829            // If the item is being opened as preview and we have an existing preview tab,
 830            // open the new item in the position of the existing preview tab.
 831            let destination_index = if allow_preview {
 832                self.close_current_preview_item(window, cx)
 833            } else {
 834                suggested_position
 835            };
 836
 837            let new_item = build_item(self, window, cx);
 838
 839            if allow_preview {
 840                self.set_preview_item_id(Some(new_item.item_id()), cx);
 841            }
 842            self.add_item_inner(
 843                new_item.clone(),
 844                true,
 845                focus_item,
 846                activate,
 847                destination_index,
 848                window,
 849                cx,
 850            );
 851
 852            new_item
 853        }
 854    }
 855
 856    pub fn close_current_preview_item(
 857        &mut self,
 858        window: &mut Window,
 859        cx: &mut Context<Self>,
 860    ) -> Option<usize> {
 861        let item_idx = self.preview_item_idx()?;
 862        let id = self.preview_item_id()?;
 863
 864        let prev_active_item_index = self.active_item_index;
 865        self.remove_item(id, false, false, window, cx);
 866        self.active_item_index = prev_active_item_index;
 867
 868        if item_idx < self.items.len() {
 869            Some(item_idx)
 870        } else {
 871            None
 872        }
 873    }
 874
 875    pub fn add_item_inner(
 876        &mut self,
 877        item: Box<dyn ItemHandle>,
 878        activate_pane: bool,
 879        focus_item: bool,
 880        activate: bool,
 881        destination_index: Option<usize>,
 882        window: &mut Window,
 883        cx: &mut Context<Self>,
 884    ) {
 885        self.close_items_over_max_tabs(window, cx);
 886
 887        if item.is_singleton(cx) {
 888            if let Some(&entry_id) = item.project_entry_ids(cx).first() {
 889                let Some(project) = self.project.upgrade() else {
 890                    return;
 891                };
 892                let project = project.read(cx);
 893                if let Some(project_path) = project.path_for_entry(entry_id, cx) {
 894                    let abs_path = project.absolute_path(&project_path, cx);
 895                    self.nav_history
 896                        .0
 897                        .lock()
 898                        .paths_by_item
 899                        .insert(item.item_id(), (project_path, abs_path));
 900                }
 901            }
 902        }
 903        // If no destination index is specified, add or move the item after the
 904        // active item (or at the start of tab bar, if the active item is pinned)
 905        let mut insertion_index = {
 906            cmp::min(
 907                if let Some(destination_index) = destination_index {
 908                    destination_index
 909                } else {
 910                    cmp::max(self.active_item_index + 1, self.pinned_count())
 911                },
 912                self.items.len(),
 913            )
 914        };
 915
 916        // Does the item already exist?
 917        let project_entry_id = if item.is_singleton(cx) {
 918            item.project_entry_ids(cx).first().copied()
 919        } else {
 920            None
 921        };
 922
 923        let existing_item_index = self.items.iter().position(|existing_item| {
 924            if existing_item.item_id() == item.item_id() {
 925                true
 926            } else if existing_item.is_singleton(cx) {
 927                existing_item
 928                    .project_entry_ids(cx)
 929                    .first()
 930                    .map_or(false, |existing_entry_id| {
 931                        Some(existing_entry_id) == project_entry_id.as_ref()
 932                    })
 933            } else {
 934                false
 935            }
 936        });
 937
 938        if let Some(existing_item_index) = existing_item_index {
 939            // If the item already exists, move it to the desired destination and activate it
 940
 941            if existing_item_index != insertion_index {
 942                let existing_item_is_active = existing_item_index == self.active_item_index;
 943
 944                // If the caller didn't specify a destination and the added item is already
 945                // the active one, don't move it
 946                if existing_item_is_active && destination_index.is_none() {
 947                    insertion_index = existing_item_index;
 948                } else {
 949                    self.items.remove(existing_item_index);
 950                    if existing_item_index < self.active_item_index {
 951                        self.active_item_index -= 1;
 952                    }
 953                    insertion_index = insertion_index.min(self.items.len());
 954
 955                    self.items.insert(insertion_index, item.clone());
 956
 957                    if existing_item_is_active {
 958                        self.active_item_index = insertion_index;
 959                    } else if insertion_index <= self.active_item_index {
 960                        self.active_item_index += 1;
 961                    }
 962                }
 963
 964                cx.notify();
 965            }
 966
 967            if activate {
 968                self.activate_item(insertion_index, activate_pane, focus_item, window, cx);
 969            }
 970        } else {
 971            self.items.insert(insertion_index, item.clone());
 972
 973            if activate {
 974                if insertion_index <= self.active_item_index
 975                    && self.preview_item_idx() != Some(self.active_item_index)
 976                {
 977                    self.active_item_index += 1;
 978                }
 979
 980                self.activate_item(insertion_index, activate_pane, focus_item, window, cx);
 981            }
 982            cx.notify();
 983        }
 984
 985        cx.emit(Event::AddItem { item });
 986    }
 987
 988    pub fn add_item(
 989        &mut self,
 990        item: Box<dyn ItemHandle>,
 991        activate_pane: bool,
 992        focus_item: bool,
 993        destination_index: Option<usize>,
 994        window: &mut Window,
 995        cx: &mut Context<Self>,
 996    ) {
 997        self.add_item_inner(
 998            item,
 999            activate_pane,
1000            focus_item,
1001            true,
1002            destination_index,
1003            window,
1004            cx,
1005        )
1006    }
1007
1008    pub fn items_len(&self) -> usize {
1009        self.items.len()
1010    }
1011
1012    pub fn items(&self) -> impl DoubleEndedIterator<Item = &Box<dyn ItemHandle>> {
1013        self.items.iter()
1014    }
1015
1016    pub fn items_of_type<T: Render>(&self) -> impl '_ + Iterator<Item = Entity<T>> {
1017        self.items
1018            .iter()
1019            .filter_map(|item| item.to_any().downcast().ok())
1020    }
1021
1022    pub fn active_item(&self) -> Option<Box<dyn ItemHandle>> {
1023        self.items.get(self.active_item_index).cloned()
1024    }
1025
1026    pub fn pixel_position_of_cursor(&self, cx: &App) -> Option<Point<Pixels>> {
1027        self.items
1028            .get(self.active_item_index)?
1029            .pixel_position_of_cursor(cx)
1030    }
1031
1032    pub fn item_for_entry(
1033        &self,
1034        entry_id: ProjectEntryId,
1035        cx: &App,
1036    ) -> Option<Box<dyn ItemHandle>> {
1037        self.items.iter().find_map(|item| {
1038            if item.is_singleton(cx) && (item.project_entry_ids(cx).as_slice() == [entry_id]) {
1039                Some(item.boxed_clone())
1040            } else {
1041                None
1042            }
1043        })
1044    }
1045
1046    pub fn item_for_path(
1047        &self,
1048        project_path: ProjectPath,
1049        cx: &App,
1050    ) -> Option<Box<dyn ItemHandle>> {
1051        self.items.iter().find_map(move |item| {
1052            if item.is_singleton(cx) && (item.project_path(cx).as_slice() == [project_path.clone()])
1053            {
1054                Some(item.boxed_clone())
1055            } else {
1056                None
1057            }
1058        })
1059    }
1060
1061    pub fn index_for_item(&self, item: &dyn ItemHandle) -> Option<usize> {
1062        self.index_for_item_id(item.item_id())
1063    }
1064
1065    fn index_for_item_id(&self, item_id: EntityId) -> Option<usize> {
1066        self.items.iter().position(|i| i.item_id() == item_id)
1067    }
1068
1069    pub fn item_for_index(&self, ix: usize) -> Option<&dyn ItemHandle> {
1070        self.items.get(ix).map(|i| i.as_ref())
1071    }
1072
1073    pub fn toggle_zoom(&mut self, _: &ToggleZoom, window: &mut Window, cx: &mut Context<Self>) {
1074        if self.zoomed {
1075            cx.emit(Event::ZoomOut);
1076        } else if !self.items.is_empty() {
1077            if !self.focus_handle.contains_focused(window, cx) {
1078                cx.focus_self(window);
1079            }
1080            cx.emit(Event::ZoomIn);
1081        }
1082    }
1083
1084    pub fn activate_item(
1085        &mut self,
1086        index: usize,
1087        activate_pane: bool,
1088        focus_item: bool,
1089        window: &mut Window,
1090        cx: &mut Context<Self>,
1091    ) {
1092        use NavigationMode::{GoingBack, GoingForward};
1093        if index < self.items.len() {
1094            let prev_active_item_ix = mem::replace(&mut self.active_item_index, index);
1095            if prev_active_item_ix != self.active_item_index
1096                || matches!(self.nav_history.mode(), GoingBack | GoingForward)
1097            {
1098                if let Some(prev_item) = self.items.get(prev_active_item_ix) {
1099                    prev_item.deactivated(window, cx);
1100                }
1101            }
1102            self.update_history(index);
1103            self.update_toolbar(window, cx);
1104            self.update_status_bar(window, cx);
1105
1106            if focus_item {
1107                self.focus_active_item(window, cx);
1108            }
1109
1110            cx.emit(Event::ActivateItem {
1111                local: activate_pane,
1112                focus_changed: focus_item,
1113            });
1114
1115            if !self.is_tab_pinned(index) {
1116                self.tab_bar_scroll_handle
1117                    .scroll_to_item(index - self.pinned_tab_count);
1118            }
1119
1120            cx.notify();
1121        }
1122    }
1123
1124    fn update_history(&mut self, index: usize) {
1125        if let Some(newly_active_item) = self.items.get(index) {
1126            self.activation_history
1127                .retain(|entry| entry.entity_id != newly_active_item.item_id());
1128            self.activation_history.push(ActivationHistoryEntry {
1129                entity_id: newly_active_item.item_id(),
1130                timestamp: self
1131                    .next_activation_timestamp
1132                    .fetch_add(1, Ordering::SeqCst),
1133            });
1134        }
1135    }
1136
1137    pub fn activate_prev_item(
1138        &mut self,
1139        activate_pane: bool,
1140        window: &mut Window,
1141        cx: &mut Context<Self>,
1142    ) {
1143        let mut index = self.active_item_index;
1144        if index > 0 {
1145            index -= 1;
1146        } else if !self.items.is_empty() {
1147            index = self.items.len() - 1;
1148        }
1149        self.activate_item(index, activate_pane, activate_pane, window, cx);
1150    }
1151
1152    pub fn activate_next_item(
1153        &mut self,
1154        activate_pane: bool,
1155        window: &mut Window,
1156        cx: &mut Context<Self>,
1157    ) {
1158        let mut index = self.active_item_index;
1159        if index + 1 < self.items.len() {
1160            index += 1;
1161        } else {
1162            index = 0;
1163        }
1164        self.activate_item(index, activate_pane, activate_pane, window, cx);
1165    }
1166
1167    pub fn swap_item_left(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1168        let index = self.active_item_index;
1169        if index == 0 {
1170            return;
1171        }
1172
1173        self.items.swap(index, index - 1);
1174        self.activate_item(index - 1, true, true, window, cx);
1175    }
1176
1177    pub fn swap_item_right(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1178        let index = self.active_item_index;
1179        if index + 1 == self.items.len() {
1180            return;
1181        }
1182
1183        self.items.swap(index, index + 1);
1184        self.activate_item(index + 1, true, true, window, cx);
1185    }
1186
1187    pub fn close_active_item(
1188        &mut self,
1189        action: &CloseActiveItem,
1190        window: &mut Window,
1191        cx: &mut Context<Self>,
1192    ) -> Option<Task<Result<()>>> {
1193        if self.items.is_empty() {
1194            // Close the window when there's no active items to close, if configured
1195            if WorkspaceSettings::get_global(cx)
1196                .when_closing_with_no_tabs
1197                .should_close()
1198            {
1199                window.dispatch_action(Box::new(CloseWindow), cx);
1200            }
1201
1202            return None;
1203        }
1204        if self.is_tab_pinned(self.active_item_index) && !action.close_pinned {
1205            // Activate any non-pinned tab in same pane
1206            let non_pinned_tab_index = self
1207                .items()
1208                .enumerate()
1209                .find(|(index, _item)| !self.is_tab_pinned(*index))
1210                .map(|(index, _item)| index);
1211            if let Some(index) = non_pinned_tab_index {
1212                self.activate_item(index, false, false, window, cx);
1213                return None;
1214            }
1215
1216            // Activate any non-pinned tab in different pane
1217            let current_pane = cx.entity();
1218            self.workspace
1219                .update(cx, |workspace, cx| {
1220                    let panes = workspace.center.panes();
1221                    let pane_with_unpinned_tab = panes.iter().find(|pane| {
1222                        if **pane == &current_pane {
1223                            return false;
1224                        }
1225                        pane.read(cx).has_unpinned_tabs()
1226                    });
1227                    if let Some(pane) = pane_with_unpinned_tab {
1228                        pane.update(cx, |pane, cx| pane.activate_unpinned_tab(window, cx));
1229                    }
1230                })
1231                .ok();
1232
1233            return None;
1234        };
1235        let active_item_id = self.items[self.active_item_index].item_id();
1236        Some(self.close_item_by_id(
1237            active_item_id,
1238            action.save_intent.unwrap_or(SaveIntent::Close),
1239            window,
1240            cx,
1241        ))
1242    }
1243
1244    pub fn close_item_by_id(
1245        &mut self,
1246        item_id_to_close: EntityId,
1247        save_intent: SaveIntent,
1248        window: &mut Window,
1249        cx: &mut Context<Self>,
1250    ) -> Task<Result<()>> {
1251        self.close_items(window, cx, save_intent, move |view_id| {
1252            view_id == item_id_to_close
1253        })
1254    }
1255
1256    pub fn close_inactive_items(
1257        &mut self,
1258        action: &CloseInactiveItems,
1259        window: &mut Window,
1260        cx: &mut Context<Self>,
1261    ) -> Option<Task<Result<()>>> {
1262        if self.items.is_empty() {
1263            return None;
1264        }
1265
1266        let active_item_id = self.items[self.active_item_index].item_id();
1267        let non_closeable_items = self.get_non_closeable_item_ids(action.close_pinned);
1268        Some(self.close_items(
1269            window,
1270            cx,
1271            action.save_intent.unwrap_or(SaveIntent::Close),
1272            move |item_id| item_id != active_item_id && !non_closeable_items.contains(&item_id),
1273        ))
1274    }
1275
1276    pub fn close_clean_items(
1277        &mut self,
1278        action: &CloseCleanItems,
1279        window: &mut Window,
1280        cx: &mut Context<Self>,
1281    ) -> Option<Task<Result<()>>> {
1282        let item_ids: Vec<_> = self
1283            .items()
1284            .filter(|item| !item.is_dirty(cx))
1285            .map(|item| item.item_id())
1286            .collect();
1287        let non_closeable_items = self.get_non_closeable_item_ids(action.close_pinned);
1288        Some(
1289            self.close_items(window, cx, SaveIntent::Close, move |item_id| {
1290                item_ids.contains(&item_id) && !non_closeable_items.contains(&item_id)
1291            }),
1292        )
1293    }
1294
1295    pub fn close_items_to_the_left(
1296        &mut self,
1297        action: &CloseItemsToTheLeft,
1298        window: &mut Window,
1299        cx: &mut Context<Self>,
1300    ) -> Option<Task<Result<()>>> {
1301        if self.items.is_empty() {
1302            return None;
1303        }
1304        let active_item_id = self.items[self.active_item_index].item_id();
1305        let non_closeable_items = self.get_non_closeable_item_ids(action.close_pinned);
1306        Some(self.close_items_to_the_left_by_id(
1307            active_item_id,
1308            action,
1309            non_closeable_items,
1310            window,
1311            cx,
1312        ))
1313    }
1314
1315    pub fn close_items_to_the_left_by_id(
1316        &mut self,
1317        item_id: EntityId,
1318        action: &CloseItemsToTheLeft,
1319        non_closeable_items: Vec<EntityId>,
1320        window: &mut Window,
1321        cx: &mut Context<Self>,
1322    ) -> Task<Result<()>> {
1323        let item_ids: Vec<_> = self
1324            .items()
1325            .take_while(|item| item.item_id() != item_id)
1326            .map(|item| item.item_id())
1327            .collect();
1328        self.close_items(window, cx, SaveIntent::Close, move |item_id| {
1329            item_ids.contains(&item_id)
1330                && !action.close_pinned
1331                && !non_closeable_items.contains(&item_id)
1332        })
1333    }
1334
1335    pub fn close_items_to_the_right(
1336        &mut self,
1337        action: &CloseItemsToTheRight,
1338        window: &mut Window,
1339        cx: &mut Context<Self>,
1340    ) -> Option<Task<Result<()>>> {
1341        if self.items.is_empty() {
1342            return None;
1343        }
1344        let active_item_id = self.items[self.active_item_index].item_id();
1345        let non_closeable_items = self.get_non_closeable_item_ids(action.close_pinned);
1346        Some(self.close_items_to_the_right_by_id(
1347            active_item_id,
1348            action,
1349            non_closeable_items,
1350            window,
1351            cx,
1352        ))
1353    }
1354
1355    pub fn close_items_to_the_right_by_id(
1356        &mut self,
1357        item_id: EntityId,
1358        action: &CloseItemsToTheRight,
1359        non_closeable_items: Vec<EntityId>,
1360        window: &mut Window,
1361        cx: &mut Context<Self>,
1362    ) -> Task<Result<()>> {
1363        let item_ids: Vec<_> = self
1364            .items()
1365            .rev()
1366            .take_while(|item| item.item_id() != item_id)
1367            .map(|item| item.item_id())
1368            .collect();
1369        self.close_items(window, cx, SaveIntent::Close, move |item_id| {
1370            item_ids.contains(&item_id)
1371                && !action.close_pinned
1372                && !non_closeable_items.contains(&item_id)
1373        })
1374    }
1375
1376    pub fn close_all_items(
1377        &mut self,
1378        action: &CloseAllItems,
1379        window: &mut Window,
1380        cx: &mut Context<Self>,
1381    ) -> Option<Task<Result<()>>> {
1382        if self.items.is_empty() {
1383            return None;
1384        }
1385
1386        let non_closeable_items = self.get_non_closeable_item_ids(action.close_pinned);
1387        Some(self.close_items(
1388            window,
1389            cx,
1390            action.save_intent.unwrap_or(SaveIntent::Close),
1391            |item_id| !non_closeable_items.contains(&item_id),
1392        ))
1393    }
1394
1395    pub fn close_items_over_max_tabs(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1396        let Some(max_tabs) = WorkspaceSettings::get_global(cx).max_tabs.map(|i| i.get()) else {
1397            return;
1398        };
1399
1400        // Reduce over the activation history to get every dirty items up to max_tabs
1401        // count.
1402        let mut index_list = Vec::new();
1403        let mut items_len = self.items_len();
1404        let mut indexes: HashMap<EntityId, usize> = HashMap::default();
1405        for (index, item) in self.items.iter().enumerate() {
1406            indexes.insert(item.item_id(), index);
1407        }
1408        for entry in self.activation_history.iter() {
1409            if items_len < max_tabs {
1410                break;
1411            }
1412            let Some(&index) = indexes.get(&entry.entity_id) else {
1413                continue;
1414            };
1415            if let Some(true) = self.items.get(index).map(|item| item.is_dirty(cx)) {
1416                continue;
1417            }
1418
1419            index_list.push(index);
1420            items_len -= 1;
1421        }
1422        // The sort and reverse is necessary since we remove items
1423        // using their index position, hence removing from the end
1424        // of the list first to avoid changing indexes.
1425        index_list.sort_unstable();
1426        index_list
1427            .iter()
1428            .rev()
1429            .for_each(|&index| self._remove_item(index, false, false, None, window, cx));
1430    }
1431
1432    // Usually when you close an item that has unsaved changes, we prompt you to
1433    // save it. That said, if you still have the buffer open in a different pane
1434    // we can close this one without fear of losing data.
1435    pub fn skip_save_on_close(item: &dyn ItemHandle, workspace: &Workspace, cx: &App) -> bool {
1436        let mut dirty_project_item_ids = Vec::new();
1437        item.for_each_project_item(cx, &mut |project_item_id, project_item| {
1438            if project_item.is_dirty() {
1439                dirty_project_item_ids.push(project_item_id);
1440            }
1441        });
1442        if dirty_project_item_ids.is_empty() {
1443            if item.is_singleton(cx) && item.is_dirty(cx) {
1444                return false;
1445            }
1446            return true;
1447        }
1448
1449        for open_item in workspace.items(cx) {
1450            if open_item.item_id() == item.item_id() {
1451                continue;
1452            }
1453            if !open_item.is_singleton(cx) {
1454                continue;
1455            }
1456            let other_project_item_ids = open_item.project_item_model_ids(cx);
1457            dirty_project_item_ids.retain(|id| !other_project_item_ids.contains(id));
1458        }
1459        if dirty_project_item_ids.is_empty() {
1460            return true;
1461        }
1462
1463        false
1464    }
1465
1466    pub(super) fn file_names_for_prompt(
1467        items: &mut dyn Iterator<Item = &Box<dyn ItemHandle>>,
1468        cx: &App,
1469    ) -> String {
1470        let mut file_names = BTreeSet::default();
1471        for item in items {
1472            item.for_each_project_item(cx, &mut |_, project_item| {
1473                if !project_item.is_dirty() {
1474                    return;
1475                }
1476                let filename = project_item.project_path(cx).and_then(|path| {
1477                    path.path
1478                        .file_name()
1479                        .and_then(|name| name.to_str().map(ToOwned::to_owned))
1480                });
1481                file_names.insert(filename.unwrap_or("untitled".to_string()));
1482            });
1483        }
1484        if file_names.len() > 6 {
1485            format!(
1486                "{}\n.. and {} more",
1487                file_names.iter().take(5).join("\n"),
1488                file_names.len() - 5
1489            )
1490        } else {
1491            file_names.into_iter().join("\n")
1492        }
1493    }
1494
1495    pub fn close_items(
1496        &mut self,
1497        window: &mut Window,
1498        cx: &mut Context<Pane>,
1499        mut save_intent: SaveIntent,
1500        should_close: impl Fn(EntityId) -> bool,
1501    ) -> Task<Result<()>> {
1502        // Find the items to close.
1503        let mut items_to_close = Vec::new();
1504        for item in &self.items {
1505            if should_close(item.item_id()) {
1506                items_to_close.push(item.boxed_clone());
1507            }
1508        }
1509
1510        let active_item_id = self.active_item().map(|item| item.item_id());
1511
1512        items_to_close.sort_by_key(|item| {
1513            let path = item.project_path(cx);
1514            // Put the currently active item at the end, because if the currently active item is not closed last
1515            // closing the currently active item will cause the focus to switch to another item
1516            // This will cause Zed to expand the content of the currently active item
1517            //
1518            // Beyond that sort in order of project path, with untitled files and multibuffers coming last.
1519            (active_item_id == Some(item.item_id()), path.is_none(), path)
1520        });
1521
1522        let workspace = self.workspace.clone();
1523        let Some(project) = self.project.upgrade() else {
1524            return Task::ready(Ok(()));
1525        };
1526        cx.spawn_in(window, async move |pane, cx| {
1527            let dirty_items = workspace.update(cx, |workspace, cx| {
1528                items_to_close
1529                    .iter()
1530                    .filter(|item| {
1531                        item.is_dirty(cx)
1532                            && !Self::skip_save_on_close(item.as_ref(), &workspace, cx)
1533                    })
1534                    .map(|item| item.boxed_clone())
1535                    .collect::<Vec<_>>()
1536            })?;
1537
1538            if save_intent == SaveIntent::Close && dirty_items.len() > 1 {
1539                let answer = pane.update_in(cx, |_, window, cx| {
1540                    let detail = Self::file_names_for_prompt(&mut dirty_items.iter(), cx);
1541                    window.prompt(
1542                        PromptLevel::Warning,
1543                        "Do you want to save changes to the following files?",
1544                        Some(&detail),
1545                        &["Save all", "Discard all", "Cancel"],
1546                        cx,
1547                    )
1548                })?;
1549                match answer.await {
1550                    Ok(0) => save_intent = SaveIntent::SaveAll,
1551                    Ok(1) => save_intent = SaveIntent::Skip,
1552                    Ok(2) => return Ok(()),
1553                    _ => {}
1554                }
1555            }
1556
1557            for item_to_close in items_to_close {
1558                let mut should_save = true;
1559                if save_intent == SaveIntent::Close {
1560                    workspace.update(cx, |workspace, cx| {
1561                        if Self::skip_save_on_close(item_to_close.as_ref(), &workspace, cx) {
1562                            should_save = false;
1563                        }
1564                    })?;
1565                }
1566
1567                if should_save {
1568                    if !Self::save_item(project.clone(), &pane, &*item_to_close, save_intent, cx)
1569                        .await?
1570                    {
1571                        break;
1572                    }
1573                }
1574
1575                // Remove the item from the pane.
1576                pane.update_in(cx, |pane, window, cx| {
1577                    pane.remove_item(
1578                        item_to_close.item_id(),
1579                        false,
1580                        pane.close_pane_if_empty,
1581                        window,
1582                        cx,
1583                    );
1584                })
1585                .ok();
1586            }
1587
1588            pane.update(cx, |_, cx| cx.notify()).ok();
1589            Ok(())
1590        })
1591    }
1592
1593    pub fn remove_item(
1594        &mut self,
1595        item_id: EntityId,
1596        activate_pane: bool,
1597        close_pane_if_empty: bool,
1598        window: &mut Window,
1599        cx: &mut Context<Self>,
1600    ) {
1601        let Some(item_index) = self.index_for_item_id(item_id) else {
1602            return;
1603        };
1604        self._remove_item(
1605            item_index,
1606            activate_pane,
1607            close_pane_if_empty,
1608            None,
1609            window,
1610            cx,
1611        )
1612    }
1613
1614    pub fn remove_item_and_focus_on_pane(
1615        &mut self,
1616        item_index: usize,
1617        activate_pane: bool,
1618        focus_on_pane_if_closed: Entity<Pane>,
1619        window: &mut Window,
1620        cx: &mut Context<Self>,
1621    ) {
1622        self._remove_item(
1623            item_index,
1624            activate_pane,
1625            true,
1626            Some(focus_on_pane_if_closed),
1627            window,
1628            cx,
1629        )
1630    }
1631
1632    fn _remove_item(
1633        &mut self,
1634        item_index: usize,
1635        activate_pane: bool,
1636        close_pane_if_empty: bool,
1637        focus_on_pane_if_closed: Option<Entity<Pane>>,
1638        window: &mut Window,
1639        cx: &mut Context<Self>,
1640    ) {
1641        let activate_on_close = &ItemSettings::get_global(cx).activate_on_close;
1642        self.activation_history
1643            .retain(|entry| entry.entity_id != self.items[item_index].item_id());
1644
1645        if self.is_tab_pinned(item_index) {
1646            self.pinned_tab_count -= 1;
1647        }
1648        if item_index == self.active_item_index {
1649            let left_neighbour_index = || item_index.min(self.items.len()).saturating_sub(1);
1650            let index_to_activate = match activate_on_close {
1651                ActivateOnClose::History => self
1652                    .activation_history
1653                    .pop()
1654                    .and_then(|last_activated_item| {
1655                        self.items.iter().enumerate().find_map(|(index, item)| {
1656                            (item.item_id() == last_activated_item.entity_id).then_some(index)
1657                        })
1658                    })
1659                    // We didn't have a valid activation history entry, so fallback
1660                    // to activating the item to the left
1661                    .unwrap_or_else(left_neighbour_index),
1662                ActivateOnClose::Neighbour => {
1663                    self.activation_history.pop();
1664                    if item_index + 1 < self.items.len() {
1665                        item_index + 1
1666                    } else {
1667                        item_index.saturating_sub(1)
1668                    }
1669                }
1670                ActivateOnClose::LeftNeighbour => {
1671                    self.activation_history.pop();
1672                    left_neighbour_index()
1673                }
1674            };
1675
1676            let should_activate = activate_pane || self.has_focus(window, cx);
1677            if self.items.len() == 1 && should_activate {
1678                self.focus_handle.focus(window);
1679            } else {
1680                self.activate_item(
1681                    index_to_activate,
1682                    should_activate,
1683                    should_activate,
1684                    window,
1685                    cx,
1686                );
1687            }
1688        }
1689
1690        let item = self.items.remove(item_index);
1691
1692        cx.emit(Event::RemovedItem { item: item.clone() });
1693        if self.items.is_empty() {
1694            item.deactivated(window, cx);
1695            if close_pane_if_empty {
1696                self.update_toolbar(window, cx);
1697                cx.emit(Event::Remove {
1698                    focus_on_pane: focus_on_pane_if_closed,
1699                });
1700            }
1701        }
1702
1703        if item_index < self.active_item_index {
1704            self.active_item_index -= 1;
1705        }
1706
1707        let mode = self.nav_history.mode();
1708        self.nav_history.set_mode(NavigationMode::ClosingItem);
1709        item.deactivated(window, cx);
1710        self.nav_history.set_mode(mode);
1711
1712        if self.is_active_preview_item(item.item_id()) {
1713            self.set_preview_item_id(None, cx);
1714        }
1715
1716        if let Some(path) = item.project_path(cx) {
1717            let abs_path = self
1718                .nav_history
1719                .0
1720                .lock()
1721                .paths_by_item
1722                .get(&item.item_id())
1723                .and_then(|(_, abs_path)| abs_path.clone());
1724
1725            self.nav_history
1726                .0
1727                .lock()
1728                .paths_by_item
1729                .insert(item.item_id(), (path, abs_path));
1730        } else {
1731            self.nav_history
1732                .0
1733                .lock()
1734                .paths_by_item
1735                .remove(&item.item_id());
1736        }
1737
1738        if self.zoom_out_on_close && self.items.is_empty() && close_pane_if_empty && self.zoomed {
1739            cx.emit(Event::ZoomOut);
1740        }
1741
1742        cx.notify();
1743    }
1744
1745    pub async fn save_item(
1746        project: Entity<Project>,
1747        pane: &WeakEntity<Pane>,
1748        item: &dyn ItemHandle,
1749        save_intent: SaveIntent,
1750        cx: &mut AsyncWindowContext,
1751    ) -> Result<bool> {
1752        const CONFLICT_MESSAGE: &str = "This file has changed on disk since you started editing it. Do you want to overwrite it?";
1753
1754        const DELETED_MESSAGE: &str = "This file has been deleted on disk since you started editing it. Do you want to recreate it?";
1755
1756        if save_intent == SaveIntent::Skip {
1757            return Ok(true);
1758        }
1759        let Some(item_ix) = pane
1760            .update(cx, |pane, _| pane.index_for_item(item))
1761            .ok()
1762            .flatten()
1763        else {
1764            return Ok(true);
1765        };
1766
1767        let (
1768            mut has_conflict,
1769            mut is_dirty,
1770            mut can_save,
1771            can_save_as,
1772            is_singleton,
1773            has_deleted_file,
1774        ) = cx.update(|_window, cx| {
1775            (
1776                item.has_conflict(cx),
1777                item.is_dirty(cx),
1778                item.can_save(cx),
1779                item.can_save_as(cx),
1780                item.is_singleton(cx),
1781                item.has_deleted_file(cx),
1782            )
1783        })?;
1784
1785        // when saving a single buffer, we ignore whether or not it's dirty.
1786        if save_intent == SaveIntent::Save || save_intent == SaveIntent::SaveWithoutFormat {
1787            is_dirty = true;
1788        }
1789
1790        if save_intent == SaveIntent::SaveAs {
1791            is_dirty = true;
1792            has_conflict = false;
1793            can_save = false;
1794        }
1795
1796        if save_intent == SaveIntent::Overwrite {
1797            has_conflict = false;
1798        }
1799
1800        let should_format = save_intent != SaveIntent::SaveWithoutFormat;
1801
1802        if has_conflict && can_save {
1803            if has_deleted_file && is_singleton {
1804                let answer = pane.update_in(cx, |pane, window, cx| {
1805                    pane.activate_item(item_ix, true, true, window, cx);
1806                    window.prompt(
1807                        PromptLevel::Warning,
1808                        DELETED_MESSAGE,
1809                        None,
1810                        &["Save", "Close", "Cancel"],
1811                        cx,
1812                    )
1813                })?;
1814                match answer.await {
1815                    Ok(0) => {
1816                        pane.update_in(cx, |_, window, cx| {
1817                            item.save(should_format, project, window, cx)
1818                        })?
1819                        .await?
1820                    }
1821                    Ok(1) => {
1822                        pane.update_in(cx, |pane, window, cx| {
1823                            pane.remove_item(item.item_id(), false, true, window, cx)
1824                        })?;
1825                    }
1826                    _ => return Ok(false),
1827                }
1828                return Ok(true);
1829            } else {
1830                let answer = pane.update_in(cx, |pane, window, cx| {
1831                    pane.activate_item(item_ix, true, true, window, cx);
1832                    window.prompt(
1833                        PromptLevel::Warning,
1834                        CONFLICT_MESSAGE,
1835                        None,
1836                        &["Overwrite", "Discard", "Cancel"],
1837                        cx,
1838                    )
1839                })?;
1840                match answer.await {
1841                    Ok(0) => {
1842                        pane.update_in(cx, |_, window, cx| {
1843                            item.save(should_format, project, window, cx)
1844                        })?
1845                        .await?
1846                    }
1847                    Ok(1) => {
1848                        pane.update_in(cx, |_, window, cx| item.reload(project, window, cx))?
1849                            .await?
1850                    }
1851                    _ => return Ok(false),
1852                }
1853            }
1854        } else if is_dirty && (can_save || can_save_as) {
1855            if save_intent == SaveIntent::Close {
1856                let will_autosave = cx.update(|_window, cx| {
1857                    matches!(
1858                        item.workspace_settings(cx).autosave,
1859                        AutosaveSetting::OnFocusChange | AutosaveSetting::OnWindowChange
1860                    ) && Self::can_autosave_item(item, cx)
1861                })?;
1862                if !will_autosave {
1863                    let item_id = item.item_id();
1864                    let answer_task = pane.update_in(cx, |pane, window, cx| {
1865                        if pane.save_modals_spawned.insert(item_id) {
1866                            pane.activate_item(item_ix, true, true, window, cx);
1867                            let prompt = dirty_message_for(item.project_path(cx));
1868                            Some(window.prompt(
1869                                PromptLevel::Warning,
1870                                &prompt,
1871                                None,
1872                                &["Save", "Don't Save", "Cancel"],
1873                                cx,
1874                            ))
1875                        } else {
1876                            None
1877                        }
1878                    })?;
1879                    if let Some(answer_task) = answer_task {
1880                        let answer = answer_task.await;
1881                        pane.update(cx, |pane, _| {
1882                            if !pane.save_modals_spawned.remove(&item_id) {
1883                                debug_panic!(
1884                                    "save modal was not present in spawned modals after awaiting for its answer"
1885                                )
1886                            }
1887                        })?;
1888                        match answer {
1889                            Ok(0) => {}
1890                            Ok(1) => {
1891                                // Don't save this file
1892                                pane.update_in(cx, |pane, window, cx| {
1893                                    if pane.is_tab_pinned(item_ix) && !item.can_save(cx) {
1894                                        pane.pinned_tab_count -= 1;
1895                                    }
1896                                    item.discarded(project, window, cx)
1897                                })
1898                                .log_err();
1899                                return Ok(true);
1900                            }
1901                            _ => return Ok(false), // Cancel
1902                        }
1903                    } else {
1904                        return Ok(false);
1905                    }
1906                }
1907            }
1908
1909            if can_save {
1910                pane.update_in(cx, |pane, window, cx| {
1911                    if pane.is_active_preview_item(item.item_id()) {
1912                        pane.set_preview_item_id(None, cx);
1913                    }
1914                    item.save(should_format, project, window, cx)
1915                })?
1916                .await?;
1917            } else if can_save_as && is_singleton {
1918                let abs_path = pane.update_in(cx, |pane, window, cx| {
1919                    pane.activate_item(item_ix, true, true, window, cx);
1920                    pane.workspace.update(cx, |workspace, cx| {
1921                        workspace.prompt_for_new_path(window, cx)
1922                    })
1923                })??;
1924                if let Some(abs_path) = abs_path.await.ok().flatten() {
1925                    pane.update_in(cx, |pane, window, cx| {
1926                        if let Some(item) = pane.item_for_path(abs_path.clone(), cx) {
1927                            pane.remove_item(item.item_id(), false, false, window, cx);
1928                        }
1929
1930                        item.save_as(project, abs_path, window, cx)
1931                    })?
1932                    .await?;
1933                } else {
1934                    return Ok(false);
1935                }
1936            }
1937        }
1938
1939        pane.update(cx, |_, cx| {
1940            cx.emit(Event::UserSavedItem {
1941                item: item.downgrade_item(),
1942                save_intent,
1943            });
1944            true
1945        })
1946    }
1947
1948    fn can_autosave_item(item: &dyn ItemHandle, cx: &App) -> bool {
1949        let is_deleted = item.project_entry_ids(cx).is_empty();
1950        item.is_dirty(cx) && !item.has_conflict(cx) && item.can_save(cx) && !is_deleted
1951    }
1952
1953    pub fn autosave_item(
1954        item: &dyn ItemHandle,
1955        project: Entity<Project>,
1956        window: &mut Window,
1957        cx: &mut App,
1958    ) -> Task<Result<()>> {
1959        let format = !matches!(
1960            item.workspace_settings(cx).autosave,
1961            AutosaveSetting::AfterDelay { .. }
1962        );
1963        if Self::can_autosave_item(item, cx) {
1964            item.save(format, project, window, cx)
1965        } else {
1966            Task::ready(Ok(()))
1967        }
1968    }
1969
1970    pub fn focus_active_item(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1971        if let Some(active_item) = self.active_item() {
1972            let focus_handle = active_item.item_focus_handle(cx);
1973            window.focus(&focus_handle);
1974        }
1975    }
1976
1977    pub fn split(&mut self, direction: SplitDirection, cx: &mut Context<Self>) {
1978        cx.emit(Event::Split(direction));
1979    }
1980
1981    pub fn toolbar(&self) -> &Entity<Toolbar> {
1982        &self.toolbar
1983    }
1984
1985    pub fn handle_deleted_project_item(
1986        &mut self,
1987        entry_id: ProjectEntryId,
1988        window: &mut Window,
1989        cx: &mut Context<Pane>,
1990    ) -> Option<()> {
1991        let item_id = self.items().find_map(|item| {
1992            if item.is_singleton(cx) && item.project_entry_ids(cx).as_slice() == [entry_id] {
1993                Some(item.item_id())
1994            } else {
1995                None
1996            }
1997        })?;
1998
1999        self.remove_item(item_id, false, true, window, cx);
2000        self.nav_history.remove_item(item_id);
2001
2002        Some(())
2003    }
2004
2005    fn update_toolbar(&mut self, window: &mut Window, cx: &mut Context<Self>) {
2006        let active_item = self
2007            .items
2008            .get(self.active_item_index)
2009            .map(|item| item.as_ref());
2010        self.toolbar.update(cx, |toolbar, cx| {
2011            toolbar.set_active_item(active_item, window, cx);
2012        });
2013    }
2014
2015    fn update_status_bar(&mut self, window: &mut Window, cx: &mut Context<Self>) {
2016        let workspace = self.workspace.clone();
2017        let pane = cx.entity().clone();
2018
2019        window.defer(cx, move |window, cx| {
2020            let Ok(status_bar) = workspace.update(cx, |workspace, _| workspace.status_bar.clone())
2021            else {
2022                return;
2023            };
2024
2025            status_bar.update(cx, move |status_bar, cx| {
2026                status_bar.set_active_pane(&pane, window, cx);
2027            });
2028        });
2029    }
2030
2031    fn entry_abs_path(&self, entry: ProjectEntryId, cx: &App) -> Option<PathBuf> {
2032        let worktree = self
2033            .workspace
2034            .upgrade()?
2035            .read(cx)
2036            .project()
2037            .read(cx)
2038            .worktree_for_entry(entry, cx)?
2039            .read(cx);
2040        let entry = worktree.entry_for_id(entry)?;
2041        match &entry.canonical_path {
2042            Some(canonical_path) => Some(canonical_path.to_path_buf()),
2043            None => worktree.absolutize(&entry.path).ok(),
2044        }
2045    }
2046
2047    pub fn icon_color(selected: bool) -> Color {
2048        if selected {
2049            Color::Default
2050        } else {
2051            Color::Muted
2052        }
2053    }
2054
2055    fn toggle_pin_tab(&mut self, _: &TogglePinTab, window: &mut Window, cx: &mut Context<Self>) {
2056        if self.items.is_empty() {
2057            return;
2058        }
2059        let active_tab_ix = self.active_item_index();
2060        if self.is_tab_pinned(active_tab_ix) {
2061            self.unpin_tab_at(active_tab_ix, window, cx);
2062        } else {
2063            self.pin_tab_at(active_tab_ix, window, cx);
2064        }
2065    }
2066
2067    fn pin_tab_at(&mut self, ix: usize, window: &mut Window, cx: &mut Context<Self>) {
2068        maybe!({
2069            let pane = cx.entity().clone();
2070            let destination_index = self.pinned_tab_count.min(ix);
2071            self.pinned_tab_count += 1;
2072            let id = self.item_for_index(ix)?.item_id();
2073
2074            if self.is_active_preview_item(id) {
2075                self.set_preview_item_id(None, cx);
2076            }
2077
2078            self.workspace
2079                .update(cx, |_, cx| {
2080                    cx.defer_in(window, move |_, window, cx| {
2081                        move_item(&pane, &pane, id, destination_index, window, cx)
2082                    });
2083                })
2084                .ok()?;
2085
2086            Some(())
2087        });
2088    }
2089
2090    fn unpin_tab_at(&mut self, ix: usize, window: &mut Window, cx: &mut Context<Self>) {
2091        maybe!({
2092            let pane = cx.entity().clone();
2093            self.pinned_tab_count = self.pinned_tab_count.checked_sub(1)?;
2094            let destination_index = self.pinned_tab_count;
2095
2096            let id = self.item_for_index(ix)?.item_id();
2097
2098            self.workspace
2099                .update(cx, |_, cx| {
2100                    cx.defer_in(window, move |_, window, cx| {
2101                        move_item(&pane, &pane, id, destination_index, window, cx)
2102                    });
2103                })
2104                .ok()?;
2105
2106            Some(())
2107        });
2108    }
2109
2110    fn is_tab_pinned(&self, ix: usize) -> bool {
2111        self.pinned_tab_count > ix
2112    }
2113
2114    fn has_pinned_tabs(&self) -> bool {
2115        self.pinned_tab_count != 0
2116    }
2117
2118    fn has_unpinned_tabs(&self) -> bool {
2119        self.pinned_tab_count < self.items.len()
2120    }
2121
2122    fn activate_unpinned_tab(&mut self, window: &mut Window, cx: &mut Context<Self>) {
2123        if self.items.is_empty() {
2124            return;
2125        }
2126        let Some(index) = self
2127            .items()
2128            .enumerate()
2129            .find_map(|(index, _item)| (!self.is_tab_pinned(index)).then_some(index))
2130        else {
2131            return;
2132        };
2133        self.activate_item(index, true, true, window, cx);
2134    }
2135
2136    fn render_tab(
2137        &self,
2138        ix: usize,
2139        item: &dyn ItemHandle,
2140        detail: usize,
2141        focus_handle: &FocusHandle,
2142        window: &mut Window,
2143        cx: &mut Context<Pane>,
2144    ) -> impl IntoElement + use<> {
2145        let is_active = ix == self.active_item_index;
2146        let is_preview = self
2147            .preview_item_id
2148            .map(|id| id == item.item_id())
2149            .unwrap_or(false);
2150
2151        let label = item.tab_content(
2152            TabContentParams {
2153                detail: Some(detail),
2154                selected: is_active,
2155                preview: is_preview,
2156                deemphasized: !self.has_focus(window, cx),
2157            },
2158            window,
2159            cx,
2160        );
2161
2162        let item_diagnostic = item
2163            .project_path(cx)
2164            .map_or(None, |project_path| self.diagnostics.get(&project_path));
2165
2166        let decorated_icon = item_diagnostic.map_or(None, |diagnostic| {
2167            let icon = match item.tab_icon(window, cx) {
2168                Some(icon) => icon,
2169                None => return None,
2170            };
2171
2172            let knockout_item_color = if is_active {
2173                cx.theme().colors().tab_active_background
2174            } else {
2175                cx.theme().colors().tab_bar_background
2176            };
2177
2178            let (icon_decoration, icon_color) = if matches!(diagnostic, &DiagnosticSeverity::ERROR)
2179            {
2180                (IconDecorationKind::X, Color::Error)
2181            } else {
2182                (IconDecorationKind::Triangle, Color::Warning)
2183            };
2184
2185            Some(DecoratedIcon::new(
2186                icon.size(IconSize::Small).color(Color::Muted),
2187                Some(
2188                    IconDecoration::new(icon_decoration, knockout_item_color, cx)
2189                        .color(icon_color.color(cx))
2190                        .position(Point {
2191                            x: px(-2.),
2192                            y: px(-2.),
2193                        }),
2194                ),
2195            ))
2196        });
2197
2198        let icon = if decorated_icon.is_none() {
2199            match item_diagnostic {
2200                Some(&DiagnosticSeverity::ERROR) => None,
2201                Some(&DiagnosticSeverity::WARNING) => None,
2202                _ => item
2203                    .tab_icon(window, cx)
2204                    .map(|icon| icon.color(Color::Muted)),
2205            }
2206            .map(|icon| icon.size(IconSize::Small))
2207        } else {
2208            None
2209        };
2210
2211        let settings = ItemSettings::get_global(cx);
2212        let close_side = &settings.close_position;
2213        let show_close_button = &settings.show_close_button;
2214        let indicator = render_item_indicator(item.boxed_clone(), cx);
2215        let item_id = item.item_id();
2216        let is_first_item = ix == 0;
2217        let is_last_item = ix == self.items.len() - 1;
2218        let is_pinned = self.is_tab_pinned(ix);
2219        let position_relative_to_active_item = ix.cmp(&self.active_item_index);
2220
2221        let tab = Tab::new(ix)
2222            .position(if is_first_item {
2223                TabPosition::First
2224            } else if is_last_item {
2225                TabPosition::Last
2226            } else {
2227                TabPosition::Middle(position_relative_to_active_item)
2228            })
2229            .close_side(match close_side {
2230                ClosePosition::Left => ui::TabCloseSide::Start,
2231                ClosePosition::Right => ui::TabCloseSide::End,
2232            })
2233            .toggle_state(is_active)
2234            .on_click(cx.listener(move |pane: &mut Self, _, window, cx| {
2235                pane.activate_item(ix, true, true, window, cx)
2236            }))
2237            // TODO: This should be a click listener with the middle mouse button instead of a mouse down listener.
2238            .on_mouse_down(
2239                MouseButton::Middle,
2240                cx.listener(move |pane, _event, window, cx| {
2241                    pane.close_item_by_id(item_id, SaveIntent::Close, window, cx)
2242                        .detach_and_log_err(cx);
2243                }),
2244            )
2245            .on_mouse_down(
2246                MouseButton::Left,
2247                cx.listener(move |pane, event: &MouseDownEvent, _, cx| {
2248                    if let Some(id) = pane.preview_item_id {
2249                        if id == item_id && event.click_count > 1 {
2250                            pane.set_preview_item_id(None, cx);
2251                        }
2252                    }
2253                }),
2254            )
2255            .on_drag(
2256                DraggedTab {
2257                    item: item.boxed_clone(),
2258                    pane: cx.entity().clone(),
2259                    detail,
2260                    is_active,
2261                    ix,
2262                },
2263                |tab, _, _, cx| cx.new(|_| tab.clone()),
2264            )
2265            .drag_over::<DraggedTab>(|tab, _, _, cx| {
2266                tab.bg(cx.theme().colors().drop_target_background)
2267            })
2268            .drag_over::<DraggedSelection>(|tab, _, _, cx| {
2269                tab.bg(cx.theme().colors().drop_target_background)
2270            })
2271            .when_some(self.can_drop_predicate.clone(), |this, p| {
2272                this.can_drop(move |a, window, cx| p(a, window, cx))
2273            })
2274            .on_drop(
2275                cx.listener(move |this, dragged_tab: &DraggedTab, window, cx| {
2276                    this.drag_split_direction = None;
2277                    this.handle_tab_drop(dragged_tab, ix, window, cx)
2278                }),
2279            )
2280            .on_drop(
2281                cx.listener(move |this, selection: &DraggedSelection, window, cx| {
2282                    this.drag_split_direction = None;
2283                    this.handle_dragged_selection_drop(selection, Some(ix), window, cx)
2284                }),
2285            )
2286            .on_drop(cx.listener(move |this, paths, window, cx| {
2287                this.drag_split_direction = None;
2288                this.handle_external_paths_drop(paths, window, cx)
2289            }))
2290            .when_some(item.tab_tooltip_content(cx), |tab, content| match content {
2291                TabTooltipContent::Text(text) => tab.tooltip(Tooltip::text(text.clone())),
2292                TabTooltipContent::Custom(element_fn) => {
2293                    tab.tooltip(move |window, cx| element_fn(window, cx))
2294                }
2295            })
2296            .start_slot::<Indicator>(indicator)
2297            .map(|this| {
2298                let end_slot_action: &'static dyn Action;
2299                let end_slot_tooltip_text: &'static str;
2300                let end_slot = if is_pinned {
2301                    end_slot_action = &TogglePinTab;
2302                    end_slot_tooltip_text = "Unpin Tab";
2303                    IconButton::new("unpin tab", IconName::Pin)
2304                        .shape(IconButtonShape::Square)
2305                        .icon_color(Color::Muted)
2306                        .size(ButtonSize::None)
2307                        .icon_size(IconSize::XSmall)
2308                        .on_click(cx.listener(move |pane, _, window, cx| {
2309                            pane.unpin_tab_at(ix, window, cx);
2310                        }))
2311                } else {
2312                    end_slot_action = &CloseActiveItem {
2313                        save_intent: None,
2314                        close_pinned: false,
2315                    };
2316                    end_slot_tooltip_text = "Close Tab";
2317                    match show_close_button {
2318                        ShowCloseButton::Always => IconButton::new("close tab", IconName::Close),
2319                        ShowCloseButton::Hover => {
2320                            IconButton::new("close tab", IconName::Close).visible_on_hover("")
2321                        }
2322                        ShowCloseButton::Hidden => return this,
2323                    }
2324                    .shape(IconButtonShape::Square)
2325                    .icon_color(Color::Muted)
2326                    .size(ButtonSize::None)
2327                    .icon_size(IconSize::XSmall)
2328                    .on_click(cx.listener(move |pane, _, window, cx| {
2329                        pane.close_item_by_id(item_id, SaveIntent::Close, window, cx)
2330                            .detach_and_log_err(cx);
2331                    }))
2332                }
2333                .map(|this| {
2334                    if is_active {
2335                        let focus_handle = focus_handle.clone();
2336                        this.tooltip(move |window, cx| {
2337                            Tooltip::for_action_in(
2338                                end_slot_tooltip_text,
2339                                end_slot_action,
2340                                &focus_handle,
2341                                window,
2342                                cx,
2343                            )
2344                        })
2345                    } else {
2346                        this.tooltip(Tooltip::text(end_slot_tooltip_text))
2347                    }
2348                });
2349                this.end_slot(end_slot)
2350            })
2351            .child(
2352                h_flex()
2353                    .gap_1()
2354                    .items_center()
2355                    .children(
2356                        std::iter::once(if let Some(decorated_icon) = decorated_icon {
2357                            Some(div().child(decorated_icon.into_any_element()))
2358                        } else if let Some(icon) = icon {
2359                            Some(div().child(icon.into_any_element()))
2360                        } else {
2361                            None
2362                        })
2363                        .flatten(),
2364                    )
2365                    .child(label),
2366            );
2367
2368        let single_entry_to_resolve = self.items[ix]
2369            .is_singleton(cx)
2370            .then(|| self.items[ix].project_entry_ids(cx).get(0).copied())
2371            .flatten();
2372
2373        let total_items = self.items.len();
2374        let has_items_to_left = ix > 0;
2375        let has_items_to_right = ix < total_items - 1;
2376        let is_pinned = self.is_tab_pinned(ix);
2377        let pane = cx.entity().downgrade();
2378        let menu_context = item.item_focus_handle(cx);
2379        right_click_menu(ix)
2380            .trigger(|_| tab)
2381            .menu(move |window, cx| {
2382                let pane = pane.clone();
2383                let menu_context = menu_context.clone();
2384                ContextMenu::build(window, cx, move |mut menu, window, cx| {
2385                    if let Some(pane) = pane.upgrade() {
2386                        menu = menu
2387                            .entry(
2388                                "Close",
2389                                Some(Box::new(CloseActiveItem {
2390                                    save_intent: None,
2391                                    close_pinned: true,
2392                                })),
2393                                window.handler_for(&pane, move |pane, window, cx| {
2394                                    pane.close_item_by_id(item_id, SaveIntent::Close, window, cx)
2395                                        .detach_and_log_err(cx);
2396                                }),
2397                            )
2398                            .item(ContextMenuItem::Entry(
2399                                ContextMenuEntry::new("Close Others")
2400                                    .action(Box::new(CloseInactiveItems {
2401                                        save_intent: None,
2402                                        close_pinned: false,
2403                                    }))
2404                                    .disabled(total_items == 1)
2405                                    .handler(window.handler_for(&pane, move |pane, window, cx| {
2406                                        pane.close_items(window, cx, SaveIntent::Close, |id| {
2407                                            id != item_id
2408                                        })
2409                                        .detach_and_log_err(cx);
2410                                    })),
2411                            ))
2412                            .separator()
2413                            .item(ContextMenuItem::Entry(
2414                                ContextMenuEntry::new("Close Left")
2415                                    .action(Box::new(CloseItemsToTheLeft {
2416                                        close_pinned: false,
2417                                    }))
2418                                    .disabled(!has_items_to_left)
2419                                    .handler(window.handler_for(&pane, move |pane, window, cx| {
2420                                        pane.close_items_to_the_left_by_id(
2421                                            item_id,
2422                                            &CloseItemsToTheLeft {
2423                                                close_pinned: false,
2424                                            },
2425                                            pane.get_non_closeable_item_ids(false),
2426                                            window,
2427                                            cx,
2428                                        )
2429                                        .detach_and_log_err(cx);
2430                                    })),
2431                            ))
2432                            .item(ContextMenuItem::Entry(
2433                                ContextMenuEntry::new("Close Right")
2434                                    .action(Box::new(CloseItemsToTheRight {
2435                                        close_pinned: false,
2436                                    }))
2437                                    .disabled(!has_items_to_right)
2438                                    .handler(window.handler_for(&pane, move |pane, window, cx| {
2439                                        pane.close_items_to_the_right_by_id(
2440                                            item_id,
2441                                            &CloseItemsToTheRight {
2442                                                close_pinned: false,
2443                                            },
2444                                            pane.get_non_closeable_item_ids(false),
2445                                            window,
2446                                            cx,
2447                                        )
2448                                        .detach_and_log_err(cx);
2449                                    })),
2450                            ))
2451                            .separator()
2452                            .entry(
2453                                "Close Clean",
2454                                Some(Box::new(CloseCleanItems {
2455                                    close_pinned: false,
2456                                })),
2457                                window.handler_for(&pane, move |pane, window, cx| {
2458                                    if let Some(task) = pane.close_clean_items(
2459                                        &CloseCleanItems {
2460                                            close_pinned: false,
2461                                        },
2462                                        window,
2463                                        cx,
2464                                    ) {
2465                                        task.detach_and_log_err(cx)
2466                                    }
2467                                }),
2468                            )
2469                            .entry(
2470                                "Close All",
2471                                Some(Box::new(CloseAllItems {
2472                                    save_intent: None,
2473                                    close_pinned: false,
2474                                })),
2475                                window.handler_for(&pane, |pane, window, cx| {
2476                                    if let Some(task) = pane.close_all_items(
2477                                        &CloseAllItems {
2478                                            save_intent: None,
2479                                            close_pinned: false,
2480                                        },
2481                                        window,
2482                                        cx,
2483                                    ) {
2484                                        task.detach_and_log_err(cx)
2485                                    }
2486                                }),
2487                            );
2488
2489                        let pin_tab_entries = |menu: ContextMenu| {
2490                            menu.separator().map(|this| {
2491                                if is_pinned {
2492                                    this.entry(
2493                                        "Unpin Tab",
2494                                        Some(TogglePinTab.boxed_clone()),
2495                                        window.handler_for(&pane, move |pane, window, cx| {
2496                                            pane.unpin_tab_at(ix, window, cx);
2497                                        }),
2498                                    )
2499                                } else {
2500                                    this.entry(
2501                                        "Pin Tab",
2502                                        Some(TogglePinTab.boxed_clone()),
2503                                        window.handler_for(&pane, move |pane, window, cx| {
2504                                            pane.pin_tab_at(ix, window, cx);
2505                                        }),
2506                                    )
2507                                }
2508                            })
2509                        };
2510                        if let Some(entry) = single_entry_to_resolve {
2511                            let project_path = pane
2512                                .read(cx)
2513                                .item_for_entry(entry, cx)
2514                                .and_then(|item| item.project_path(cx));
2515                            let worktree = project_path.as_ref().and_then(|project_path| {
2516                                pane.read(cx)
2517                                    .project
2518                                    .upgrade()?
2519                                    .read(cx)
2520                                    .worktree_for_id(project_path.worktree_id, cx)
2521                            });
2522                            let has_relative_path = worktree.as_ref().is_some_and(|worktree| {
2523                                worktree
2524                                    .read(cx)
2525                                    .root_entry()
2526                                    .map_or(false, |entry| entry.is_dir())
2527                            });
2528
2529                            let entry_abs_path = pane.read(cx).entry_abs_path(entry, cx);
2530                            let parent_abs_path = entry_abs_path
2531                                .as_deref()
2532                                .and_then(|abs_path| Some(abs_path.parent()?.to_path_buf()));
2533                            let relative_path = project_path
2534                                .map(|project_path| project_path.path)
2535                                .filter(|_| has_relative_path);
2536
2537                            let visible_in_project_panel = relative_path.is_some()
2538                                && worktree.is_some_and(|worktree| worktree.read(cx).is_visible());
2539
2540                            let entry_id = entry.to_proto();
2541                            menu = menu
2542                                .separator()
2543                                .when_some(entry_abs_path, |menu, abs_path| {
2544                                    menu.entry(
2545                                        "Copy Path",
2546                                        Some(Box::new(zed_actions::workspace::CopyPath)),
2547                                        window.handler_for(&pane, move |_, _, cx| {
2548                                            cx.write_to_clipboard(ClipboardItem::new_string(
2549                                                abs_path.to_string_lossy().to_string(),
2550                                            ));
2551                                        }),
2552                                    )
2553                                })
2554                                .when_some(relative_path, |menu, relative_path| {
2555                                    menu.entry(
2556                                        "Copy Relative Path",
2557                                        Some(Box::new(zed_actions::workspace::CopyRelativePath)),
2558                                        window.handler_for(&pane, move |_, _, cx| {
2559                                            cx.write_to_clipboard(ClipboardItem::new_string(
2560                                                relative_path.to_string_lossy().to_string(),
2561                                            ));
2562                                        }),
2563                                    )
2564                                })
2565                                .map(pin_tab_entries)
2566                                .separator()
2567                                .when(visible_in_project_panel, |menu| {
2568                                    menu.entry(
2569                                        "Reveal In Project Panel",
2570                                        Some(Box::new(RevealInProjectPanel {
2571                                            entry_id: Some(entry_id),
2572                                        })),
2573                                        window.handler_for(&pane, move |pane, _, cx| {
2574                                            pane.project
2575                                                .update(cx, |_, cx| {
2576                                                    cx.emit(project::Event::RevealInProjectPanel(
2577                                                        ProjectEntryId::from_proto(entry_id),
2578                                                    ))
2579                                                })
2580                                                .ok();
2581                                        }),
2582                                    )
2583                                })
2584                                .when_some(parent_abs_path, |menu, parent_abs_path| {
2585                                    menu.entry(
2586                                        "Open in Terminal",
2587                                        Some(Box::new(OpenInTerminal)),
2588                                        window.handler_for(&pane, move |_, window, cx| {
2589                                            window.dispatch_action(
2590                                                OpenTerminal {
2591                                                    working_directory: parent_abs_path.clone(),
2592                                                }
2593                                                .boxed_clone(),
2594                                                cx,
2595                                            );
2596                                        }),
2597                                    )
2598                                });
2599                        } else {
2600                            menu = menu.map(pin_tab_entries);
2601                        }
2602                    }
2603
2604                    menu.context(menu_context)
2605                })
2606            })
2607    }
2608
2609    fn render_tab_bar(&mut self, window: &mut Window, cx: &mut Context<Pane>) -> AnyElement {
2610        let focus_handle = self.focus_handle.clone();
2611        let navigate_backward = IconButton::new("navigate_backward", IconName::ArrowLeft)
2612            .icon_size(IconSize::Small)
2613            .on_click({
2614                let entity = cx.entity().clone();
2615                move |_, window, cx| {
2616                    entity.update(cx, |pane, cx| pane.navigate_backward(window, cx))
2617                }
2618            })
2619            .disabled(!self.can_navigate_backward())
2620            .tooltip({
2621                let focus_handle = focus_handle.clone();
2622                move |window, cx| {
2623                    Tooltip::for_action_in("Go Back", &GoBack, &focus_handle, window, cx)
2624                }
2625            });
2626
2627        let navigate_forward = IconButton::new("navigate_forward", IconName::ArrowRight)
2628            .icon_size(IconSize::Small)
2629            .on_click({
2630                let entity = cx.entity().clone();
2631                move |_, window, cx| entity.update(cx, |pane, cx| pane.navigate_forward(window, cx))
2632            })
2633            .disabled(!self.can_navigate_forward())
2634            .tooltip({
2635                let focus_handle = focus_handle.clone();
2636                move |window, cx| {
2637                    Tooltip::for_action_in("Go Forward", &GoForward, &focus_handle, window, cx)
2638                }
2639            });
2640
2641        let mut tab_items = self
2642            .items
2643            .iter()
2644            .enumerate()
2645            .zip(tab_details(&self.items, window, cx))
2646            .map(|((ix, item), detail)| {
2647                self.render_tab(ix, &**item, detail, &focus_handle, window, cx)
2648            })
2649            .collect::<Vec<_>>();
2650        let tab_count = tab_items.len();
2651        let unpinned_tabs = tab_items.split_off(self.pinned_tab_count);
2652        let pinned_tabs = tab_items;
2653        TabBar::new("tab_bar")
2654            .when(
2655                self.display_nav_history_buttons.unwrap_or_default(),
2656                |tab_bar| {
2657                    tab_bar
2658                        .start_child(navigate_backward)
2659                        .start_child(navigate_forward)
2660                },
2661            )
2662            .map(|tab_bar| {
2663                if self.show_tab_bar_buttons {
2664                    let render_tab_buttons = self.render_tab_bar_buttons.clone();
2665                    let (left_children, right_children) = render_tab_buttons(self, window, cx);
2666                    tab_bar
2667                        .start_children(left_children)
2668                        .end_children(right_children)
2669                } else {
2670                    tab_bar
2671                }
2672            })
2673            .children(pinned_tabs.len().ne(&0).then(|| {
2674                let content_width = self.tab_bar_scroll_handle.content_size().width;
2675                let viewport_width = self.tab_bar_scroll_handle.viewport().size.width;
2676                // We need to check both because offset returns delta values even when the scroll handle is not scrollable
2677                let is_scrollable = content_width > viewport_width;
2678                let is_scrolled = self.tab_bar_scroll_handle.offset().x < px(0.);
2679                let has_active_unpinned_tab = self.active_item_index >= self.pinned_tab_count;
2680                h_flex()
2681                    .children(pinned_tabs)
2682                    .when(is_scrollable && is_scrolled, |this| {
2683                        this.when(has_active_unpinned_tab, |this| this.border_r_2())
2684                            .when(!has_active_unpinned_tab, |this| this.border_r_1())
2685                            .border_color(cx.theme().colors().border)
2686                    })
2687            }))
2688            .child(
2689                h_flex()
2690                    .id("unpinned tabs")
2691                    .overflow_x_scroll()
2692                    .w_full()
2693                    .track_scroll(&self.tab_bar_scroll_handle)
2694                    .children(unpinned_tabs)
2695                    .child(
2696                        div()
2697                            .id("tab_bar_drop_target")
2698                            .min_w_6()
2699                            // HACK: This empty child is currently necessary to force the drop target to appear
2700                            // despite us setting a min width above.
2701                            .child("")
2702                            .h_full()
2703                            .flex_grow()
2704                            .drag_over::<DraggedTab>(|bar, _, _, cx| {
2705                                bar.bg(cx.theme().colors().drop_target_background)
2706                            })
2707                            .drag_over::<DraggedSelection>(|bar, _, _, cx| {
2708                                bar.bg(cx.theme().colors().drop_target_background)
2709                            })
2710                            .on_drop(cx.listener(
2711                                move |this, dragged_tab: &DraggedTab, window, cx| {
2712                                    this.drag_split_direction = None;
2713                                    this.handle_tab_drop(dragged_tab, this.items.len(), window, cx)
2714                                },
2715                            ))
2716                            .on_drop(cx.listener(
2717                                move |this, selection: &DraggedSelection, window, cx| {
2718                                    this.drag_split_direction = None;
2719                                    this.handle_project_entry_drop(
2720                                        &selection.active_selection.entry_id,
2721                                        Some(tab_count),
2722                                        window,
2723                                        cx,
2724                                    )
2725                                },
2726                            ))
2727                            .on_drop(cx.listener(move |this, paths, window, cx| {
2728                                this.drag_split_direction = None;
2729                                this.handle_external_paths_drop(paths, window, cx)
2730                            }))
2731                            .on_click(cx.listener(move |this, event: &ClickEvent, window, cx| {
2732                                if event.up.click_count == 2 {
2733                                    window.dispatch_action(
2734                                        this.double_click_dispatch_action.boxed_clone(),
2735                                        cx,
2736                                    );
2737                                }
2738                            })),
2739                    ),
2740            )
2741            .into_any_element()
2742    }
2743
2744    pub fn render_menu_overlay(menu: &Entity<ContextMenu>) -> Div {
2745        div().absolute().bottom_0().right_0().size_0().child(
2746            deferred(anchored().anchor(Corner::TopRight).child(menu.clone())).with_priority(1),
2747        )
2748    }
2749
2750    pub fn set_zoomed(&mut self, zoomed: bool, cx: &mut Context<Self>) {
2751        self.zoomed = zoomed;
2752        cx.notify();
2753    }
2754
2755    pub fn is_zoomed(&self) -> bool {
2756        self.zoomed
2757    }
2758
2759    fn handle_drag_move<T: 'static>(
2760        &mut self,
2761        event: &DragMoveEvent<T>,
2762        window: &mut Window,
2763        cx: &mut Context<Self>,
2764    ) {
2765        let can_split_predicate = self.can_split_predicate.take();
2766        let can_split = match &can_split_predicate {
2767            Some(can_split_predicate) => {
2768                can_split_predicate(self, event.dragged_item(), window, cx)
2769            }
2770            None => false,
2771        };
2772        self.can_split_predicate = can_split_predicate;
2773        if !can_split {
2774            return;
2775        }
2776
2777        let rect = event.bounds.size;
2778
2779        let size = event.bounds.size.width.min(event.bounds.size.height)
2780            * WorkspaceSettings::get_global(cx).drop_target_size;
2781
2782        let relative_cursor = Point::new(
2783            event.event.position.x - event.bounds.left(),
2784            event.event.position.y - event.bounds.top(),
2785        );
2786
2787        let direction = if relative_cursor.x < size
2788            || relative_cursor.x > rect.width - size
2789            || relative_cursor.y < size
2790            || relative_cursor.y > rect.height - size
2791        {
2792            [
2793                SplitDirection::Up,
2794                SplitDirection::Right,
2795                SplitDirection::Down,
2796                SplitDirection::Left,
2797            ]
2798            .iter()
2799            .min_by_key(|side| match side {
2800                SplitDirection::Up => relative_cursor.y,
2801                SplitDirection::Right => rect.width - relative_cursor.x,
2802                SplitDirection::Down => rect.height - relative_cursor.y,
2803                SplitDirection::Left => relative_cursor.x,
2804            })
2805            .cloned()
2806        } else {
2807            None
2808        };
2809
2810        if direction != self.drag_split_direction {
2811            self.drag_split_direction = direction;
2812        }
2813    }
2814
2815    pub fn handle_tab_drop(
2816        &mut self,
2817        dragged_tab: &DraggedTab,
2818        ix: usize,
2819        window: &mut Window,
2820        cx: &mut Context<Self>,
2821    ) {
2822        if let Some(custom_drop_handle) = self.custom_drop_handle.clone() {
2823            if let ControlFlow::Break(()) = custom_drop_handle(self, dragged_tab, window, cx) {
2824                return;
2825            }
2826        }
2827        let mut to_pane = cx.entity().clone();
2828        let split_direction = self.drag_split_direction;
2829        let item_id = dragged_tab.item.item_id();
2830        if let Some(preview_item_id) = self.preview_item_id {
2831            if item_id == preview_item_id {
2832                self.set_preview_item_id(None, cx);
2833            }
2834        }
2835
2836        let from_pane = dragged_tab.pane.clone();
2837        self.workspace
2838            .update(cx, |_, cx| {
2839                cx.defer_in(window, move |workspace, window, cx| {
2840                    if let Some(split_direction) = split_direction {
2841                        to_pane = workspace.split_pane(to_pane, split_direction, window, cx);
2842                    }
2843                    let old_ix = from_pane.read(cx).index_for_item_id(item_id);
2844                    let old_len = to_pane.read(cx).items.len();
2845                    move_item(&from_pane, &to_pane, item_id, ix, window, cx);
2846                    if to_pane == from_pane {
2847                        if let Some(old_index) = old_ix {
2848                            to_pane.update(cx, |this, _| {
2849                                if old_index < this.pinned_tab_count
2850                                    && (ix == this.items.len() || ix > this.pinned_tab_count)
2851                                {
2852                                    this.pinned_tab_count -= 1;
2853                                } else if this.has_pinned_tabs()
2854                                    && old_index >= this.pinned_tab_count
2855                                    && ix < this.pinned_tab_count
2856                                {
2857                                    this.pinned_tab_count += 1;
2858                                }
2859                            });
2860                        }
2861                    } else {
2862                        to_pane.update(cx, |this, _| {
2863                            if this.items.len() > old_len // Did we not deduplicate on drag?
2864                                && this.has_pinned_tabs()
2865                                && ix < this.pinned_tab_count
2866                            {
2867                                this.pinned_tab_count += 1;
2868                            }
2869                        });
2870                        from_pane.update(cx, |this, _| {
2871                            if let Some(index) = old_ix {
2872                                if this.pinned_tab_count > index {
2873                                    this.pinned_tab_count -= 1;
2874                                }
2875                            }
2876                        })
2877                    }
2878                });
2879            })
2880            .log_err();
2881    }
2882
2883    fn handle_dragged_selection_drop(
2884        &mut self,
2885        dragged_selection: &DraggedSelection,
2886        dragged_onto: Option<usize>,
2887        window: &mut Window,
2888        cx: &mut Context<Self>,
2889    ) {
2890        if let Some(custom_drop_handle) = self.custom_drop_handle.clone() {
2891            if let ControlFlow::Break(()) = custom_drop_handle(self, dragged_selection, window, cx)
2892            {
2893                return;
2894            }
2895        }
2896        self.handle_project_entry_drop(
2897            &dragged_selection.active_selection.entry_id,
2898            dragged_onto,
2899            window,
2900            cx,
2901        );
2902    }
2903
2904    fn handle_project_entry_drop(
2905        &mut self,
2906        project_entry_id: &ProjectEntryId,
2907        target: Option<usize>,
2908        window: &mut Window,
2909        cx: &mut Context<Self>,
2910    ) {
2911        if let Some(custom_drop_handle) = self.custom_drop_handle.clone() {
2912            if let ControlFlow::Break(()) = custom_drop_handle(self, project_entry_id, window, cx) {
2913                return;
2914            }
2915        }
2916        let mut to_pane = cx.entity().clone();
2917        let split_direction = self.drag_split_direction;
2918        let project_entry_id = *project_entry_id;
2919        self.workspace
2920            .update(cx, |_, cx| {
2921                cx.defer_in(window, move |workspace, window, cx| {
2922                    if let Some(path) = workspace
2923                        .project()
2924                        .read(cx)
2925                        .path_for_entry(project_entry_id, cx)
2926                    {
2927                        let load_path_task = workspace.load_path(path, window, cx);
2928                        cx.spawn_in(window, async move |workspace, cx| {
2929                            if let Some((project_entry_id, build_item)) =
2930                                load_path_task.await.notify_async_err(cx)
2931                            {
2932                                let (to_pane, new_item_handle) = workspace
2933                                    .update_in(cx, |workspace, window, cx| {
2934                                        if let Some(split_direction) = split_direction {
2935                                            to_pane = workspace.split_pane(
2936                                                to_pane,
2937                                                split_direction,
2938                                                window,
2939                                                cx,
2940                                            );
2941                                        }
2942                                        let new_item_handle = to_pane.update(cx, |pane, cx| {
2943                                            pane.open_item(
2944                                                project_entry_id,
2945                                                true,
2946                                                false,
2947                                                true,
2948                                                target,
2949                                                window,
2950                                                cx,
2951                                                build_item,
2952                                            )
2953                                        });
2954                                        (to_pane, new_item_handle)
2955                                    })
2956                                    .log_err()?;
2957                                to_pane
2958                                    .update_in(cx, |this, window, cx| {
2959                                        let Some(index) = this.index_for_item(&*new_item_handle)
2960                                        else {
2961                                            return;
2962                                        };
2963
2964                                        if target.map_or(false, |target| this.is_tab_pinned(target))
2965                                        {
2966                                            this.pin_tab_at(index, window, cx);
2967                                        }
2968                                    })
2969                                    .ok()?
2970                            }
2971                            Some(())
2972                        })
2973                        .detach();
2974                    };
2975                });
2976            })
2977            .log_err();
2978    }
2979
2980    fn handle_external_paths_drop(
2981        &mut self,
2982        paths: &ExternalPaths,
2983        window: &mut Window,
2984        cx: &mut Context<Self>,
2985    ) {
2986        if let Some(custom_drop_handle) = self.custom_drop_handle.clone() {
2987            if let ControlFlow::Break(()) = custom_drop_handle(self, paths, window, cx) {
2988                return;
2989            }
2990        }
2991        let mut to_pane = cx.entity().clone();
2992        let mut split_direction = self.drag_split_direction;
2993        let paths = paths.paths().to_vec();
2994        let is_remote = self
2995            .workspace
2996            .update(cx, |workspace, cx| {
2997                if workspace.project().read(cx).is_via_collab() {
2998                    workspace.show_error(
2999                        &anyhow::anyhow!("Cannot drop files on a remote project"),
3000                        cx,
3001                    );
3002                    true
3003                } else {
3004                    false
3005                }
3006            })
3007            .unwrap_or(true);
3008        if is_remote {
3009            return;
3010        }
3011
3012        self.workspace
3013            .update(cx, |workspace, cx| {
3014                let fs = Arc::clone(workspace.project().read(cx).fs());
3015                cx.spawn_in(window, async move |workspace, cx| {
3016                    let mut is_file_checks = FuturesUnordered::new();
3017                    for path in &paths {
3018                        is_file_checks.push(fs.is_file(path))
3019                    }
3020                    let mut has_files_to_open = false;
3021                    while let Some(is_file) = is_file_checks.next().await {
3022                        if is_file {
3023                            has_files_to_open = true;
3024                            break;
3025                        }
3026                    }
3027                    drop(is_file_checks);
3028                    if !has_files_to_open {
3029                        split_direction = None;
3030                    }
3031
3032                    if let Ok(open_task) = workspace.update_in(cx, |workspace, window, cx| {
3033                        if let Some(split_direction) = split_direction {
3034                            to_pane = workspace.split_pane(to_pane, split_direction, window, cx);
3035                        }
3036                        workspace.open_paths(
3037                            paths,
3038                            OpenOptions {
3039                                visible: Some(OpenVisible::OnlyDirectories),
3040                                ..Default::default()
3041                            },
3042                            Some(to_pane.downgrade()),
3043                            window,
3044                            cx,
3045                        )
3046                    }) {
3047                        let opened_items: Vec<_> = open_task.await;
3048                        _ = workspace.update(cx, |workspace, cx| {
3049                            for item in opened_items.into_iter().flatten() {
3050                                if let Err(e) = item {
3051                                    workspace.show_error(&e, cx);
3052                                }
3053                            }
3054                        });
3055                    }
3056                })
3057                .detach();
3058            })
3059            .log_err();
3060    }
3061
3062    pub fn display_nav_history_buttons(&mut self, display: Option<bool>) {
3063        self.display_nav_history_buttons = display;
3064    }
3065
3066    fn get_non_closeable_item_ids(&self, close_pinned: bool) -> Vec<EntityId> {
3067        if close_pinned {
3068            return vec![];
3069        }
3070
3071        self.items
3072            .iter()
3073            .enumerate()
3074            .filter(|(index, _item)| self.is_tab_pinned(*index))
3075            .map(|(_, item)| item.item_id())
3076            .collect()
3077    }
3078
3079    pub fn drag_split_direction(&self) -> Option<SplitDirection> {
3080        self.drag_split_direction
3081    }
3082
3083    pub fn set_zoom_out_on_close(&mut self, zoom_out_on_close: bool) {
3084        self.zoom_out_on_close = zoom_out_on_close;
3085    }
3086}
3087
3088fn default_render_tab_bar_buttons(
3089    pane: &mut Pane,
3090    window: &mut Window,
3091    cx: &mut Context<Pane>,
3092) -> (Option<AnyElement>, Option<AnyElement>) {
3093    if !pane.has_focus(window, cx) && !pane.context_menu_focused(window, cx) {
3094        return (None, None);
3095    }
3096    // Ideally we would return a vec of elements here to pass directly to the [TabBar]'s
3097    // `end_slot`, but due to needing a view here that isn't possible.
3098    let right_children = h_flex()
3099        // Instead we need to replicate the spacing from the [TabBar]'s `end_slot` here.
3100        .gap(DynamicSpacing::Base04.rems(cx))
3101        .child(
3102            PopoverMenu::new("pane-tab-bar-popover-menu")
3103                .trigger_with_tooltip(
3104                    IconButton::new("plus", IconName::Plus).icon_size(IconSize::Small),
3105                    Tooltip::text("New..."),
3106                )
3107                .anchor(Corner::TopRight)
3108                .with_handle(pane.new_item_context_menu_handle.clone())
3109                .menu(move |window, cx| {
3110                    Some(ContextMenu::build(window, cx, |menu, _, _| {
3111                        menu.action("New File", NewFile.boxed_clone())
3112                            .action("Open File", ToggleFileFinder::default().boxed_clone())
3113                            .separator()
3114                            .action(
3115                                "Search Project",
3116                                DeploySearch {
3117                                    replace_enabled: false,
3118                                    included_files: None,
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}