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