pane.rs

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