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