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