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