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