pane.rs

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