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