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