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