pane.rs

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