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_unpinned_tabs(&self) -> bool {
2178        self.pinned_tab_count < self.items.len()
2179    }
2180
2181    fn activate_unpinned_tab(&mut self, window: &mut Window, cx: &mut Context<Self>) {
2182        if self.items.is_empty() {
2183            return;
2184        }
2185        let Some(index) = self
2186            .items()
2187            .enumerate()
2188            .find_map(|(index, _item)| (!self.is_tab_pinned(index)).then_some(index))
2189        else {
2190            return;
2191        };
2192        self.activate_item(index, true, true, window, cx);
2193    }
2194
2195    fn render_tab(
2196        &self,
2197        ix: usize,
2198        item: &dyn ItemHandle,
2199        detail: usize,
2200        focus_handle: &FocusHandle,
2201        window: &mut Window,
2202        cx: &mut Context<Pane>,
2203    ) -> impl IntoElement + use<> {
2204        let is_active = ix == self.active_item_index;
2205        let is_preview = self
2206            .preview_item_id
2207            .map(|id| id == item.item_id())
2208            .unwrap_or(false);
2209
2210        let label = item.tab_content(
2211            TabContentParams {
2212                detail: Some(detail),
2213                selected: is_active,
2214                preview: is_preview,
2215                deemphasized: !self.has_focus(window, cx),
2216            },
2217            window,
2218            cx,
2219        );
2220
2221        let item_diagnostic = item
2222            .project_path(cx)
2223            .map_or(None, |project_path| self.diagnostics.get(&project_path));
2224
2225        let decorated_icon = item_diagnostic.map_or(None, |diagnostic| {
2226            let icon = match item.tab_icon(window, cx) {
2227                Some(icon) => icon,
2228                None => return None,
2229            };
2230
2231            let knockout_item_color = if is_active {
2232                cx.theme().colors().tab_active_background
2233            } else {
2234                cx.theme().colors().tab_bar_background
2235            };
2236
2237            let (icon_decoration, icon_color) = if matches!(diagnostic, &DiagnosticSeverity::ERROR)
2238            {
2239                (IconDecorationKind::X, Color::Error)
2240            } else {
2241                (IconDecorationKind::Triangle, Color::Warning)
2242            };
2243
2244            Some(DecoratedIcon::new(
2245                icon.size(IconSize::Small).color(Color::Muted),
2246                Some(
2247                    IconDecoration::new(icon_decoration, knockout_item_color, cx)
2248                        .color(icon_color.color(cx))
2249                        .position(Point {
2250                            x: px(-2.),
2251                            y: px(-2.),
2252                        }),
2253                ),
2254            ))
2255        });
2256
2257        let icon = if decorated_icon.is_none() {
2258            match item_diagnostic {
2259                Some(&DiagnosticSeverity::ERROR) => None,
2260                Some(&DiagnosticSeverity::WARNING) => None,
2261                _ => item
2262                    .tab_icon(window, cx)
2263                    .map(|icon| icon.color(Color::Muted)),
2264            }
2265            .map(|icon| icon.size(IconSize::Small))
2266        } else {
2267            None
2268        };
2269
2270        let settings = ItemSettings::get_global(cx);
2271        let close_side = &settings.close_position;
2272        let show_close_button = &settings.show_close_button;
2273        let indicator = render_item_indicator(item.boxed_clone(), cx);
2274        let item_id = item.item_id();
2275        let is_first_item = ix == 0;
2276        let is_last_item = ix == self.items.len() - 1;
2277        let is_pinned = self.is_tab_pinned(ix);
2278        let position_relative_to_active_item = ix.cmp(&self.active_item_index);
2279
2280        let tab = Tab::new(ix)
2281            .position(if is_first_item {
2282                TabPosition::First
2283            } else if is_last_item {
2284                TabPosition::Last
2285            } else {
2286                TabPosition::Middle(position_relative_to_active_item)
2287            })
2288            .close_side(match close_side {
2289                ClosePosition::Left => ui::TabCloseSide::Start,
2290                ClosePosition::Right => ui::TabCloseSide::End,
2291            })
2292            .toggle_state(is_active)
2293            .on_click(cx.listener(move |pane: &mut Self, _, window, cx| {
2294                pane.activate_item(ix, true, true, window, cx)
2295            }))
2296            // TODO: This should be a click listener with the middle mouse button instead of a mouse down listener.
2297            .on_mouse_down(
2298                MouseButton::Middle,
2299                cx.listener(move |pane, _event, window, cx| {
2300                    pane.close_item_by_id(item_id, SaveIntent::Close, window, cx)
2301                        .detach_and_log_err(cx);
2302                }),
2303            )
2304            .on_mouse_down(
2305                MouseButton::Left,
2306                cx.listener(move |pane, event: &MouseDownEvent, _, cx| {
2307                    if let Some(id) = pane.preview_item_id {
2308                        if id == item_id && event.click_count > 1 {
2309                            pane.set_preview_item_id(None, cx);
2310                        }
2311                    }
2312                }),
2313            )
2314            .on_drag(
2315                DraggedTab {
2316                    item: item.boxed_clone(),
2317                    pane: cx.entity().clone(),
2318                    detail,
2319                    is_active,
2320                    ix,
2321                },
2322                |tab, _, _, cx| cx.new(|_| tab.clone()),
2323            )
2324            .drag_over::<DraggedTab>(|tab, _, _, cx| {
2325                tab.bg(cx.theme().colors().drop_target_background)
2326            })
2327            .drag_over::<DraggedSelection>(|tab, _, _, cx| {
2328                tab.bg(cx.theme().colors().drop_target_background)
2329            })
2330            .when_some(self.can_drop_predicate.clone(), |this, p| {
2331                this.can_drop(move |a, window, cx| p(a, window, cx))
2332            })
2333            .on_drop(
2334                cx.listener(move |this, dragged_tab: &DraggedTab, window, cx| {
2335                    this.drag_split_direction = None;
2336                    this.handle_tab_drop(dragged_tab, ix, window, cx)
2337                }),
2338            )
2339            .on_drop(
2340                cx.listener(move |this, selection: &DraggedSelection, window, cx| {
2341                    this.drag_split_direction = None;
2342                    this.handle_dragged_selection_drop(selection, Some(ix), window, cx)
2343                }),
2344            )
2345            .on_drop(cx.listener(move |this, paths, window, cx| {
2346                this.drag_split_direction = None;
2347                this.handle_external_paths_drop(paths, window, cx)
2348            }))
2349            .when_some(item.tab_tooltip_content(cx), |tab, content| match content {
2350                TabTooltipContent::Text(text) => tab.tooltip(Tooltip::text(text.clone())),
2351                TabTooltipContent::Custom(element_fn) => {
2352                    tab.tooltip(move |window, cx| element_fn(window, cx))
2353                }
2354            })
2355            .start_slot::<Indicator>(indicator)
2356            .map(|this| {
2357                let end_slot_action: &'static dyn Action;
2358                let end_slot_tooltip_text: &'static str;
2359                let end_slot = if is_pinned {
2360                    end_slot_action = &TogglePinTab;
2361                    end_slot_tooltip_text = "Unpin Tab";
2362                    IconButton::new("unpin tab", IconName::Pin)
2363                        .shape(IconButtonShape::Square)
2364                        .icon_color(Color::Muted)
2365                        .size(ButtonSize::None)
2366                        .icon_size(IconSize::XSmall)
2367                        .on_click(cx.listener(move |pane, _, window, cx| {
2368                            pane.unpin_tab_at(ix, window, cx);
2369                        }))
2370                } else {
2371                    end_slot_action = &CloseActiveItem {
2372                        save_intent: None,
2373                        close_pinned: false,
2374                    };
2375                    end_slot_tooltip_text = "Close Tab";
2376                    match show_close_button {
2377                        ShowCloseButton::Always => IconButton::new("close tab", IconName::Close),
2378                        ShowCloseButton::Hover => {
2379                            IconButton::new("close tab", IconName::Close).visible_on_hover("")
2380                        }
2381                        ShowCloseButton::Hidden => return this,
2382                    }
2383                    .shape(IconButtonShape::Square)
2384                    .icon_color(Color::Muted)
2385                    .size(ButtonSize::None)
2386                    .icon_size(IconSize::XSmall)
2387                    .on_click(cx.listener(move |pane, _, window, cx| {
2388                        pane.close_item_by_id(item_id, SaveIntent::Close, window, cx)
2389                            .detach_and_log_err(cx);
2390                    }))
2391                }
2392                .map(|this| {
2393                    if is_active {
2394                        let focus_handle = focus_handle.clone();
2395                        this.tooltip(move |window, cx| {
2396                            Tooltip::for_action_in(
2397                                end_slot_tooltip_text,
2398                                end_slot_action,
2399                                &focus_handle,
2400                                window,
2401                                cx,
2402                            )
2403                        })
2404                    } else {
2405                        this.tooltip(Tooltip::text(end_slot_tooltip_text))
2406                    }
2407                });
2408                this.end_slot(end_slot)
2409            })
2410            .child(
2411                h_flex()
2412                    .gap_1()
2413                    .items_center()
2414                    .children(
2415                        std::iter::once(if let Some(decorated_icon) = decorated_icon {
2416                            Some(div().child(decorated_icon.into_any_element()))
2417                        } else if let Some(icon) = icon {
2418                            Some(div().child(icon.into_any_element()))
2419                        } else {
2420                            None
2421                        })
2422                        .flatten(),
2423                    )
2424                    .child(label),
2425            );
2426
2427        let single_entry_to_resolve = self.items[ix]
2428            .is_singleton(cx)
2429            .then(|| self.items[ix].project_entry_ids(cx).get(0).copied())
2430            .flatten();
2431
2432        let total_items = self.items.len();
2433        let has_items_to_left = ix > 0;
2434        let has_items_to_right = ix < total_items - 1;
2435        let has_clean_items = self.items.iter().any(|item| !item.is_dirty(cx));
2436        let is_pinned = self.is_tab_pinned(ix);
2437        let pane = cx.entity().downgrade();
2438        let menu_context = item.item_focus_handle(cx);
2439        right_click_menu(ix)
2440            .trigger(|_| tab)
2441            .menu(move |window, cx| {
2442                let pane = pane.clone();
2443                let menu_context = menu_context.clone();
2444                ContextMenu::build(window, cx, move |mut menu, window, cx| {
2445                    let close_active_item_action = CloseActiveItem {
2446                        save_intent: None,
2447                        close_pinned: true,
2448                    };
2449                    let close_inactive_items_action = CloseInactiveItems {
2450                        save_intent: None,
2451                        close_pinned: false,
2452                    };
2453                    let close_items_to_the_left_action = CloseItemsToTheLeft {
2454                        close_pinned: false,
2455                    };
2456                    let close_items_to_the_right_action = CloseItemsToTheRight {
2457                        close_pinned: false,
2458                    };
2459                    let close_clean_items_action = CloseCleanItems {
2460                        close_pinned: false,
2461                    };
2462                    let close_all_items_action = CloseAllItems {
2463                        save_intent: None,
2464                        close_pinned: false,
2465                    };
2466                    if let Some(pane) = pane.upgrade() {
2467                        menu = menu
2468                            .entry(
2469                                "Close",
2470                                Some(Box::new(close_active_item_action)),
2471                                window.handler_for(&pane, move |pane, window, cx| {
2472                                    pane.close_item_by_id(item_id, SaveIntent::Close, window, cx)
2473                                        .detach_and_log_err(cx);
2474                                }),
2475                            )
2476                            .item(ContextMenuItem::Entry(
2477                                ContextMenuEntry::new("Close Others")
2478                                    .action(Box::new(close_inactive_items_action.clone()))
2479                                    .disabled(total_items == 1)
2480                                    .handler(window.handler_for(&pane, move |pane, window, cx| {
2481                                        pane.close_inactive_items(
2482                                            &close_inactive_items_action,
2483                                            window,
2484                                            cx,
2485                                        )
2486                                        .detach_and_log_err(cx);
2487                                    })),
2488                            ))
2489                            .separator()
2490                            .item(ContextMenuItem::Entry(
2491                                ContextMenuEntry::new("Close Left")
2492                                    .action(Box::new(close_items_to_the_left_action.clone()))
2493                                    .disabled(!has_items_to_left)
2494                                    .handler(window.handler_for(&pane, move |pane, window, cx| {
2495                                        pane.close_items_to_the_left_by_id(
2496                                            Some(item_id),
2497                                            &close_items_to_the_left_action,
2498                                            window,
2499                                            cx,
2500                                        )
2501                                        .detach_and_log_err(cx);
2502                                    })),
2503                            ))
2504                            .item(ContextMenuItem::Entry(
2505                                ContextMenuEntry::new("Close Right")
2506                                    .action(Box::new(close_items_to_the_right_action.clone()))
2507                                    .disabled(!has_items_to_right)
2508                                    .handler(window.handler_for(&pane, move |pane, window, cx| {
2509                                        pane.close_items_to_the_right_by_id(
2510                                            Some(item_id),
2511                                            &close_items_to_the_right_action,
2512                                            window,
2513                                            cx,
2514                                        )
2515                                        .detach_and_log_err(cx);
2516                                    })),
2517                            ))
2518                            .separator()
2519                            .item(ContextMenuItem::Entry(
2520                                ContextMenuEntry::new("Close Clean")
2521                                    .action(Box::new(close_clean_items_action.clone()))
2522                                    .disabled(!has_clean_items)
2523                                    .handler(window.handler_for(&pane, move |pane, window, cx| {
2524                                        pane.close_clean_items(
2525                                            &close_clean_items_action,
2526                                            window,
2527                                            cx,
2528                                        )
2529                                        .detach_and_log_err(cx)
2530                                    })),
2531                            ))
2532                            .entry(
2533                                "Close All",
2534                                Some(Box::new(close_all_items_action.clone())),
2535                                window.handler_for(&pane, move |pane, window, cx| {
2536                                    pane.close_all_items(&close_all_items_action, window, cx)
2537                                        .detach_and_log_err(cx)
2538                                }),
2539                            );
2540
2541                        let pin_tab_entries = |menu: ContextMenu| {
2542                            menu.separator().map(|this| {
2543                                if is_pinned {
2544                                    this.entry(
2545                                        "Unpin Tab",
2546                                        Some(TogglePinTab.boxed_clone()),
2547                                        window.handler_for(&pane, move |pane, window, cx| {
2548                                            pane.unpin_tab_at(ix, window, cx);
2549                                        }),
2550                                    )
2551                                } else {
2552                                    this.entry(
2553                                        "Pin Tab",
2554                                        Some(TogglePinTab.boxed_clone()),
2555                                        window.handler_for(&pane, move |pane, window, cx| {
2556                                            pane.pin_tab_at(ix, window, cx);
2557                                        }),
2558                                    )
2559                                }
2560                            })
2561                        };
2562                        if let Some(entry) = single_entry_to_resolve {
2563                            let project_path = pane
2564                                .read(cx)
2565                                .item_for_entry(entry, cx)
2566                                .and_then(|item| item.project_path(cx));
2567                            let worktree = project_path.as_ref().and_then(|project_path| {
2568                                pane.read(cx)
2569                                    .project
2570                                    .upgrade()?
2571                                    .read(cx)
2572                                    .worktree_for_id(project_path.worktree_id, cx)
2573                            });
2574                            let has_relative_path = worktree.as_ref().is_some_and(|worktree| {
2575                                worktree
2576                                    .read(cx)
2577                                    .root_entry()
2578                                    .map_or(false, |entry| entry.is_dir())
2579                            });
2580
2581                            let entry_abs_path = pane.read(cx).entry_abs_path(entry, cx);
2582                            let parent_abs_path = entry_abs_path
2583                                .as_deref()
2584                                .and_then(|abs_path| Some(abs_path.parent()?.to_path_buf()));
2585                            let relative_path = project_path
2586                                .map(|project_path| project_path.path)
2587                                .filter(|_| has_relative_path);
2588
2589                            let visible_in_project_panel = relative_path.is_some()
2590                                && worktree.is_some_and(|worktree| worktree.read(cx).is_visible());
2591
2592                            let entry_id = entry.to_proto();
2593                            menu = menu
2594                                .separator()
2595                                .when_some(entry_abs_path, |menu, abs_path| {
2596                                    menu.entry(
2597                                        "Copy Path",
2598                                        Some(Box::new(zed_actions::workspace::CopyPath)),
2599                                        window.handler_for(&pane, move |_, _, cx| {
2600                                            cx.write_to_clipboard(ClipboardItem::new_string(
2601                                                abs_path.to_string_lossy().to_string(),
2602                                            ));
2603                                        }),
2604                                    )
2605                                })
2606                                .when_some(relative_path, |menu, relative_path| {
2607                                    menu.entry(
2608                                        "Copy Relative Path",
2609                                        Some(Box::new(zed_actions::workspace::CopyRelativePath)),
2610                                        window.handler_for(&pane, move |_, _, cx| {
2611                                            cx.write_to_clipboard(ClipboardItem::new_string(
2612                                                relative_path.to_string_lossy().to_string(),
2613                                            ));
2614                                        }),
2615                                    )
2616                                })
2617                                .map(pin_tab_entries)
2618                                .separator()
2619                                .when(visible_in_project_panel, |menu| {
2620                                    menu.entry(
2621                                        "Reveal In Project Panel",
2622                                        Some(Box::new(RevealInProjectPanel {
2623                                            entry_id: Some(entry_id),
2624                                        })),
2625                                        window.handler_for(&pane, move |pane, _, cx| {
2626                                            pane.project
2627                                                .update(cx, |_, cx| {
2628                                                    cx.emit(project::Event::RevealInProjectPanel(
2629                                                        ProjectEntryId::from_proto(entry_id),
2630                                                    ))
2631                                                })
2632                                                .ok();
2633                                        }),
2634                                    )
2635                                })
2636                                .when_some(parent_abs_path, |menu, parent_abs_path| {
2637                                    menu.entry(
2638                                        "Open in Terminal",
2639                                        Some(Box::new(OpenInTerminal)),
2640                                        window.handler_for(&pane, move |_, window, cx| {
2641                                            window.dispatch_action(
2642                                                OpenTerminal {
2643                                                    working_directory: parent_abs_path.clone(),
2644                                                }
2645                                                .boxed_clone(),
2646                                                cx,
2647                                            );
2648                                        }),
2649                                    )
2650                                });
2651                        } else {
2652                            menu = menu.map(pin_tab_entries);
2653                        }
2654                    }
2655
2656                    menu.context(menu_context)
2657                })
2658            })
2659    }
2660
2661    fn render_tab_bar(&mut self, window: &mut Window, cx: &mut Context<Pane>) -> AnyElement {
2662        let focus_handle = self.focus_handle.clone();
2663        let navigate_backward = IconButton::new("navigate_backward", IconName::ArrowLeft)
2664            .icon_size(IconSize::Small)
2665            .on_click({
2666                let entity = cx.entity().clone();
2667                move |_, window, cx| {
2668                    entity.update(cx, |pane, cx| pane.navigate_backward(window, cx))
2669                }
2670            })
2671            .disabled(!self.can_navigate_backward())
2672            .tooltip({
2673                let focus_handle = focus_handle.clone();
2674                move |window, cx| {
2675                    Tooltip::for_action_in("Go Back", &GoBack, &focus_handle, window, cx)
2676                }
2677            });
2678
2679        let navigate_forward = IconButton::new("navigate_forward", IconName::ArrowRight)
2680            .icon_size(IconSize::Small)
2681            .on_click({
2682                let entity = cx.entity().clone();
2683                move |_, window, cx| entity.update(cx, |pane, cx| pane.navigate_forward(window, cx))
2684            })
2685            .disabled(!self.can_navigate_forward())
2686            .tooltip({
2687                let focus_handle = focus_handle.clone();
2688                move |window, cx| {
2689                    Tooltip::for_action_in("Go Forward", &GoForward, &focus_handle, window, cx)
2690                }
2691            });
2692
2693        let mut tab_items = self
2694            .items
2695            .iter()
2696            .enumerate()
2697            .zip(tab_details(&self.items, window, cx))
2698            .map(|((ix, item), detail)| {
2699                self.render_tab(ix, &**item, detail, &focus_handle, window, cx)
2700            })
2701            .collect::<Vec<_>>();
2702        let tab_count = tab_items.len();
2703        let unpinned_tabs = tab_items.split_off(self.pinned_tab_count);
2704        let pinned_tabs = tab_items;
2705        TabBar::new("tab_bar")
2706            .when(
2707                self.display_nav_history_buttons.unwrap_or_default(),
2708                |tab_bar| {
2709                    tab_bar
2710                        .start_child(navigate_backward)
2711                        .start_child(navigate_forward)
2712                },
2713            )
2714            .map(|tab_bar| {
2715                if self.show_tab_bar_buttons {
2716                    let render_tab_buttons = self.render_tab_bar_buttons.clone();
2717                    let (left_children, right_children) = render_tab_buttons(self, window, cx);
2718                    tab_bar
2719                        .start_children(left_children)
2720                        .end_children(right_children)
2721                } else {
2722                    tab_bar
2723                }
2724            })
2725            .children(pinned_tabs.len().ne(&0).then(|| {
2726                let content_width = self.tab_bar_scroll_handle.content_size().width;
2727                let viewport_width = self.tab_bar_scroll_handle.viewport().size.width;
2728                // We need to check both because offset returns delta values even when the scroll handle is not scrollable
2729                let is_scrollable = content_width > viewport_width;
2730                let is_scrolled = self.tab_bar_scroll_handle.offset().x < px(0.);
2731                let has_active_unpinned_tab = self.active_item_index >= self.pinned_tab_count;
2732                h_flex()
2733                    .children(pinned_tabs)
2734                    .when(is_scrollable && is_scrolled, |this| {
2735                        this.when(has_active_unpinned_tab, |this| this.border_r_2())
2736                            .when(!has_active_unpinned_tab, |this| this.border_r_1())
2737                            .border_color(cx.theme().colors().border)
2738                    })
2739            }))
2740            .child(
2741                h_flex()
2742                    .id("unpinned tabs")
2743                    .overflow_x_scroll()
2744                    .w_full()
2745                    .track_scroll(&self.tab_bar_scroll_handle)
2746                    .children(unpinned_tabs)
2747                    .child(
2748                        div()
2749                            .id("tab_bar_drop_target")
2750                            .min_w_6()
2751                            // HACK: This empty child is currently necessary to force the drop target to appear
2752                            // despite us setting a min width above.
2753                            .child("")
2754                            .h_full()
2755                            .flex_grow()
2756                            .drag_over::<DraggedTab>(|bar, _, _, cx| {
2757                                bar.bg(cx.theme().colors().drop_target_background)
2758                            })
2759                            .drag_over::<DraggedSelection>(|bar, _, _, cx| {
2760                                bar.bg(cx.theme().colors().drop_target_background)
2761                            })
2762                            .on_drop(cx.listener(
2763                                move |this, dragged_tab: &DraggedTab, window, cx| {
2764                                    this.drag_split_direction = None;
2765                                    this.handle_tab_drop(dragged_tab, this.items.len(), window, cx)
2766                                },
2767                            ))
2768                            .on_drop(cx.listener(
2769                                move |this, selection: &DraggedSelection, window, cx| {
2770                                    this.drag_split_direction = None;
2771                                    this.handle_project_entry_drop(
2772                                        &selection.active_selection.entry_id,
2773                                        Some(tab_count),
2774                                        window,
2775                                        cx,
2776                                    )
2777                                },
2778                            ))
2779                            .on_drop(cx.listener(move |this, paths, window, cx| {
2780                                this.drag_split_direction = None;
2781                                this.handle_external_paths_drop(paths, window, cx)
2782                            }))
2783                            .on_click(cx.listener(move |this, event: &ClickEvent, window, cx| {
2784                                if event.up.click_count == 2 {
2785                                    window.dispatch_action(
2786                                        this.double_click_dispatch_action.boxed_clone(),
2787                                        cx,
2788                                    );
2789                                }
2790                            })),
2791                    ),
2792            )
2793            .into_any_element()
2794    }
2795
2796    pub fn render_menu_overlay(menu: &Entity<ContextMenu>) -> Div {
2797        div().absolute().bottom_0().right_0().size_0().child(
2798            deferred(anchored().anchor(Corner::TopRight).child(menu.clone())).with_priority(1),
2799        )
2800    }
2801
2802    pub fn set_zoomed(&mut self, zoomed: bool, cx: &mut Context<Self>) {
2803        self.zoomed = zoomed;
2804        cx.notify();
2805    }
2806
2807    pub fn is_zoomed(&self) -> bool {
2808        self.zoomed
2809    }
2810
2811    fn handle_drag_move<T: 'static>(
2812        &mut self,
2813        event: &DragMoveEvent<T>,
2814        window: &mut Window,
2815        cx: &mut Context<Self>,
2816    ) {
2817        let can_split_predicate = self.can_split_predicate.take();
2818        let can_split = match &can_split_predicate {
2819            Some(can_split_predicate) => {
2820                can_split_predicate(self, event.dragged_item(), window, cx)
2821            }
2822            None => false,
2823        };
2824        self.can_split_predicate = can_split_predicate;
2825        if !can_split {
2826            return;
2827        }
2828
2829        let rect = event.bounds.size;
2830
2831        let size = event.bounds.size.width.min(event.bounds.size.height)
2832            * WorkspaceSettings::get_global(cx).drop_target_size;
2833
2834        let relative_cursor = Point::new(
2835            event.event.position.x - event.bounds.left(),
2836            event.event.position.y - event.bounds.top(),
2837        );
2838
2839        let direction = if relative_cursor.x < size
2840            || relative_cursor.x > rect.width - size
2841            || relative_cursor.y < size
2842            || relative_cursor.y > rect.height - size
2843        {
2844            [
2845                SplitDirection::Up,
2846                SplitDirection::Right,
2847                SplitDirection::Down,
2848                SplitDirection::Left,
2849            ]
2850            .iter()
2851            .min_by_key(|side| match side {
2852                SplitDirection::Up => relative_cursor.y,
2853                SplitDirection::Right => rect.width - relative_cursor.x,
2854                SplitDirection::Down => rect.height - relative_cursor.y,
2855                SplitDirection::Left => relative_cursor.x,
2856            })
2857            .cloned()
2858        } else {
2859            None
2860        };
2861
2862        if direction != self.drag_split_direction {
2863            self.drag_split_direction = direction;
2864        }
2865    }
2866
2867    pub fn handle_tab_drop(
2868        &mut self,
2869        dragged_tab: &DraggedTab,
2870        ix: usize,
2871        window: &mut Window,
2872        cx: &mut Context<Self>,
2873    ) {
2874        if let Some(custom_drop_handle) = self.custom_drop_handle.clone() {
2875            if let ControlFlow::Break(()) = custom_drop_handle(self, dragged_tab, window, cx) {
2876                return;
2877            }
2878        }
2879        let mut to_pane = cx.entity().clone();
2880        let split_direction = self.drag_split_direction;
2881        let item_id = dragged_tab.item.item_id();
2882        if let Some(preview_item_id) = self.preview_item_id {
2883            if item_id == preview_item_id {
2884                self.set_preview_item_id(None, cx);
2885            }
2886        }
2887
2888        let is_clone = cfg!(target_os = "macos") && window.modifiers().alt
2889            || cfg!(not(target_os = "macos")) && window.modifiers().control;
2890
2891        let from_pane = dragged_tab.pane.clone();
2892        let from_ix = dragged_tab.ix;
2893        self.workspace
2894            .update(cx, |_, cx| {
2895                cx.defer_in(window, move |workspace, window, cx| {
2896                    if let Some(split_direction) = split_direction {
2897                        to_pane = workspace.split_pane(to_pane, split_direction, window, cx);
2898                    }
2899                    let database_id = workspace.database_id();
2900                    let was_pinned_in_from_pane = from_pane.read_with(cx, |pane, _| {
2901                        pane.index_for_item_id(item_id)
2902                            .is_some_and(|ix| pane.is_tab_pinned(ix))
2903                    });
2904                    let to_pane_old_length = 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                    to_pane.update(cx, |this, _| {
2923                        if to_pane == from_pane {
2924                            let moved_right = ix > from_ix;
2925                            let ix = if moved_right { ix - 1 } else { ix };
2926                            let is_pinned_in_to_pane = this.is_tab_pinned(ix);
2927
2928                            if !was_pinned_in_from_pane && is_pinned_in_to_pane {
2929                                this.pinned_tab_count += 1;
2930                            } else if was_pinned_in_from_pane && !is_pinned_in_to_pane {
2931                                this.pinned_tab_count -= 1;
2932                            }
2933                        } else if this.items.len() >= to_pane_old_length {
2934                            let is_pinned_in_to_pane = this.is_tab_pinned(ix);
2935                            let item_created_pane = to_pane_old_length == 0;
2936                            let is_first_position = ix == 0;
2937                            let was_dropped_at_beginning = item_created_pane || is_first_position;
2938                            let should_remain_pinned = is_pinned_in_to_pane
2939                                || (was_pinned_in_from_pane && was_dropped_at_beginning);
2940
2941                            if should_remain_pinned {
2942                                this.pinned_tab_count += 1;
2943                            }
2944                        }
2945                    });
2946                });
2947            })
2948            .log_err();
2949    }
2950
2951    fn handle_dragged_selection_drop(
2952        &mut self,
2953        dragged_selection: &DraggedSelection,
2954        dragged_onto: Option<usize>,
2955        window: &mut Window,
2956        cx: &mut Context<Self>,
2957    ) {
2958        if let Some(custom_drop_handle) = self.custom_drop_handle.clone() {
2959            if let ControlFlow::Break(()) = custom_drop_handle(self, dragged_selection, window, cx)
2960            {
2961                return;
2962            }
2963        }
2964        self.handle_project_entry_drop(
2965            &dragged_selection.active_selection.entry_id,
2966            dragged_onto,
2967            window,
2968            cx,
2969        );
2970    }
2971
2972    fn handle_project_entry_drop(
2973        &mut self,
2974        project_entry_id: &ProjectEntryId,
2975        target: Option<usize>,
2976        window: &mut Window,
2977        cx: &mut Context<Self>,
2978    ) {
2979        if let Some(custom_drop_handle) = self.custom_drop_handle.clone() {
2980            if let ControlFlow::Break(()) = custom_drop_handle(self, project_entry_id, window, cx) {
2981                return;
2982            }
2983        }
2984        let mut to_pane = cx.entity().clone();
2985        let split_direction = self.drag_split_direction;
2986        let project_entry_id = *project_entry_id;
2987        self.workspace
2988            .update(cx, |_, cx| {
2989                cx.defer_in(window, move |workspace, window, cx| {
2990                    if let Some(project_path) = workspace
2991                        .project()
2992                        .read(cx)
2993                        .path_for_entry(project_entry_id, cx)
2994                    {
2995                        let load_path_task = workspace.load_path(project_path.clone(), window, cx);
2996                        cx.spawn_in(window, async move |workspace, cx| {
2997                            if let Some((project_entry_id, build_item)) =
2998                                load_path_task.await.notify_async_err(cx)
2999                            {
3000                                let (to_pane, new_item_handle) = workspace
3001                                    .update_in(cx, |workspace, window, cx| {
3002                                        if let Some(split_direction) = split_direction {
3003                                            to_pane = workspace.split_pane(
3004                                                to_pane,
3005                                                split_direction,
3006                                                window,
3007                                                cx,
3008                                            );
3009                                        }
3010                                        let new_item_handle = to_pane.update(cx, |pane, cx| {
3011                                            pane.open_item(
3012                                                project_entry_id,
3013                                                project_path,
3014                                                true,
3015                                                false,
3016                                                true,
3017                                                target,
3018                                                window,
3019                                                cx,
3020                                                build_item,
3021                                            )
3022                                        });
3023                                        (to_pane, new_item_handle)
3024                                    })
3025                                    .log_err()?;
3026                                to_pane
3027                                    .update_in(cx, |this, window, cx| {
3028                                        let Some(index) = this.index_for_item(&*new_item_handle)
3029                                        else {
3030                                            return;
3031                                        };
3032
3033                                        if target.map_or(false, |target| this.is_tab_pinned(target))
3034                                        {
3035                                            this.pin_tab_at(index, window, cx);
3036                                        }
3037                                    })
3038                                    .ok()?
3039                            }
3040                            Some(())
3041                        })
3042                        .detach();
3043                    };
3044                });
3045            })
3046            .log_err();
3047    }
3048
3049    fn handle_external_paths_drop(
3050        &mut self,
3051        paths: &ExternalPaths,
3052        window: &mut Window,
3053        cx: &mut Context<Self>,
3054    ) {
3055        if let Some(custom_drop_handle) = self.custom_drop_handle.clone() {
3056            if let ControlFlow::Break(()) = custom_drop_handle(self, paths, window, cx) {
3057                return;
3058            }
3059        }
3060        let mut to_pane = cx.entity().clone();
3061        let mut split_direction = self.drag_split_direction;
3062        let paths = paths.paths().to_vec();
3063        let is_remote = self
3064            .workspace
3065            .update(cx, |workspace, cx| {
3066                if workspace.project().read(cx).is_via_collab() {
3067                    workspace.show_error(
3068                        &anyhow::anyhow!("Cannot drop files on a remote project"),
3069                        cx,
3070                    );
3071                    true
3072                } else {
3073                    false
3074                }
3075            })
3076            .unwrap_or(true);
3077        if is_remote {
3078            return;
3079        }
3080
3081        self.workspace
3082            .update(cx, |workspace, cx| {
3083                let fs = Arc::clone(workspace.project().read(cx).fs());
3084                cx.spawn_in(window, async move |workspace, cx| {
3085                    let mut is_file_checks = FuturesUnordered::new();
3086                    for path in &paths {
3087                        is_file_checks.push(fs.is_file(path))
3088                    }
3089                    let mut has_files_to_open = false;
3090                    while let Some(is_file) = is_file_checks.next().await {
3091                        if is_file {
3092                            has_files_to_open = true;
3093                            break;
3094                        }
3095                    }
3096                    drop(is_file_checks);
3097                    if !has_files_to_open {
3098                        split_direction = None;
3099                    }
3100
3101                    if let Ok(open_task) = workspace.update_in(cx, |workspace, window, cx| {
3102                        if let Some(split_direction) = split_direction {
3103                            to_pane = workspace.split_pane(to_pane, split_direction, window, cx);
3104                        }
3105                        workspace.open_paths(
3106                            paths,
3107                            OpenOptions {
3108                                visible: Some(OpenVisible::OnlyDirectories),
3109                                ..Default::default()
3110                            },
3111                            Some(to_pane.downgrade()),
3112                            window,
3113                            cx,
3114                        )
3115                    }) {
3116                        let opened_items: Vec<_> = open_task.await;
3117                        _ = workspace.update(cx, |workspace, cx| {
3118                            for item in opened_items.into_iter().flatten() {
3119                                if let Err(e) = item {
3120                                    workspace.show_error(&e, cx);
3121                                }
3122                            }
3123                        });
3124                    }
3125                })
3126                .detach();
3127            })
3128            .log_err();
3129    }
3130
3131    pub fn display_nav_history_buttons(&mut self, display: Option<bool>) {
3132        self.display_nav_history_buttons = display;
3133    }
3134
3135    fn pinned_item_ids(&self) -> HashSet<EntityId> {
3136        self.items
3137            .iter()
3138            .enumerate()
3139            .filter_map(|(index, item)| {
3140                if self.is_tab_pinned(index) {
3141                    return Some(item.item_id());
3142                }
3143
3144                None
3145            })
3146            .collect()
3147    }
3148
3149    fn clean_item_ids(&self, cx: &mut Context<Pane>) -> HashSet<EntityId> {
3150        self.items()
3151            .filter_map(|item| {
3152                if !item.is_dirty(cx) {
3153                    return Some(item.item_id());
3154                }
3155
3156                None
3157            })
3158            .collect()
3159    }
3160
3161    fn to_the_side_item_ids(&self, item_id: EntityId, side: Side) -> HashSet<EntityId> {
3162        match side {
3163            Side::Left => self
3164                .items()
3165                .take_while(|item| item.item_id() != item_id)
3166                .map(|item| item.item_id())
3167                .collect(),
3168            Side::Right => self
3169                .items()
3170                .rev()
3171                .take_while(|item| item.item_id() != item_id)
3172                .map(|item| item.item_id())
3173                .collect(),
3174        }
3175    }
3176
3177    pub fn drag_split_direction(&self) -> Option<SplitDirection> {
3178        self.drag_split_direction
3179    }
3180
3181    pub fn set_zoom_out_on_close(&mut self, zoom_out_on_close: bool) {
3182        self.zoom_out_on_close = zoom_out_on_close;
3183    }
3184}
3185
3186fn default_render_tab_bar_buttons(
3187    pane: &mut Pane,
3188    window: &mut Window,
3189    cx: &mut Context<Pane>,
3190) -> (Option<AnyElement>, Option<AnyElement>) {
3191    if !pane.has_focus(window, cx) && !pane.context_menu_focused(window, cx) {
3192        return (None, None);
3193    }
3194    // Ideally we would return a vec of elements here to pass directly to the [TabBar]'s
3195    // `end_slot`, but due to needing a view here that isn't possible.
3196    let right_children = h_flex()
3197        // Instead we need to replicate the spacing from the [TabBar]'s `end_slot` here.
3198        .gap(DynamicSpacing::Base04.rems(cx))
3199        .child(
3200            PopoverMenu::new("pane-tab-bar-popover-menu")
3201                .trigger_with_tooltip(
3202                    IconButton::new("plus", IconName::Plus).icon_size(IconSize::Small),
3203                    Tooltip::text("New..."),
3204                )
3205                .anchor(Corner::TopRight)
3206                .with_handle(pane.new_item_context_menu_handle.clone())
3207                .menu(move |window, cx| {
3208                    Some(ContextMenu::build(window, cx, |menu, _, _| {
3209                        menu.action("New File", NewFile.boxed_clone())
3210                            .action("Open File", ToggleFileFinder::default().boxed_clone())
3211                            .separator()
3212                            .action(
3213                                "Search Project",
3214                                DeploySearch {
3215                                    replace_enabled: false,
3216                                    included_files: None,
3217                                    excluded_files: None,
3218                                }
3219                                .boxed_clone(),
3220                            )
3221                            .action("Search Symbols", ToggleProjectSymbols.boxed_clone())
3222                            .separator()
3223                            .action("New Terminal", NewTerminal.boxed_clone())
3224                    }))
3225                }),
3226        )
3227        .child(
3228            PopoverMenu::new("pane-tab-bar-split")
3229                .trigger_with_tooltip(
3230                    IconButton::new("split", IconName::Split).icon_size(IconSize::Small),
3231                    Tooltip::text("Split Pane"),
3232                )
3233                .anchor(Corner::TopRight)
3234                .with_handle(pane.split_item_context_menu_handle.clone())
3235                .menu(move |window, cx| {
3236                    ContextMenu::build(window, cx, |menu, _, _| {
3237                        menu.action("Split Right", SplitRight.boxed_clone())
3238                            .action("Split Left", SplitLeft.boxed_clone())
3239                            .action("Split Up", SplitUp.boxed_clone())
3240                            .action("Split Down", SplitDown.boxed_clone())
3241                    })
3242                    .into()
3243                }),
3244        )
3245        .child({
3246            let zoomed = pane.is_zoomed();
3247            IconButton::new("toggle_zoom", IconName::Maximize)
3248                .icon_size(IconSize::Small)
3249                .toggle_state(zoomed)
3250                .selected_icon(IconName::Minimize)
3251                .on_click(cx.listener(|pane, _, window, cx| {
3252                    pane.toggle_zoom(&crate::ToggleZoom, window, cx);
3253                }))
3254                .tooltip(move |window, cx| {
3255                    Tooltip::for_action(
3256                        if zoomed { "Zoom Out" } else { "Zoom In" },
3257                        &ToggleZoom,
3258                        window,
3259                        cx,
3260                    )
3261                })
3262        })
3263        .into_any_element()
3264        .into();
3265    (None, right_children)
3266}
3267
3268impl Focusable for Pane {
3269    fn focus_handle(&self, _cx: &App) -> FocusHandle {
3270        self.focus_handle.clone()
3271    }
3272}
3273
3274impl Render for Pane {
3275    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
3276        let mut key_context = KeyContext::new_with_defaults();
3277        key_context.add("Pane");
3278        if self.active_item().is_none() {
3279            key_context.add("EmptyPane");
3280        }
3281
3282        let should_display_tab_bar = self.should_display_tab_bar.clone();
3283        let display_tab_bar = should_display_tab_bar(window, cx);
3284        let Some(project) = self.project.upgrade() else {
3285            return div().track_focus(&self.focus_handle(cx));
3286        };
3287        let is_local = project.read(cx).is_local();
3288
3289        v_flex()
3290            .key_context(key_context)
3291            .track_focus(&self.focus_handle(cx))
3292            .size_full()
3293            .flex_none()
3294            .overflow_hidden()
3295            .on_action(cx.listener(|pane, _: &AlternateFile, window, cx| {
3296                pane.alternate_file(window, cx);
3297            }))
3298            .on_action(
3299                cx.listener(|pane, _: &SplitLeft, _, cx| pane.split(SplitDirection::Left, cx)),
3300            )
3301            .on_action(cx.listener(|pane, _: &SplitUp, _, cx| pane.split(SplitDirection::Up, cx)))
3302            .on_action(cx.listener(|pane, _: &SplitHorizontal, _, cx| {
3303                pane.split(SplitDirection::horizontal(cx), cx)
3304            }))
3305            .on_action(cx.listener(|pane, _: &SplitVertical, _, cx| {
3306                pane.split(SplitDirection::vertical(cx), cx)
3307            }))
3308            .on_action(
3309                cx.listener(|pane, _: &SplitRight, _, cx| pane.split(SplitDirection::Right, cx)),
3310            )
3311            .on_action(
3312                cx.listener(|pane, _: &SplitDown, _, cx| pane.split(SplitDirection::Down, cx)),
3313            )
3314            .on_action(
3315                cx.listener(|pane, _: &GoBack, window, cx| pane.navigate_backward(window, cx)),
3316            )
3317            .on_action(
3318                cx.listener(|pane, _: &GoForward, window, cx| pane.navigate_forward(window, cx)),
3319            )
3320            .on_action(cx.listener(|_, _: &JoinIntoNext, _, cx| {
3321                cx.emit(Event::JoinIntoNext);
3322            }))
3323            .on_action(cx.listener(|_, _: &JoinAll, _, cx| {
3324                cx.emit(Event::JoinAll);
3325            }))
3326            .on_action(cx.listener(Pane::toggle_zoom))
3327            .on_action(
3328                cx.listener(|pane: &mut Pane, action: &ActivateItem, window, cx| {
3329                    pane.activate_item(
3330                        action.0.min(pane.items.len().saturating_sub(1)),
3331                        true,
3332                        true,
3333                        window,
3334                        cx,
3335                    );
3336                }),
3337            )
3338            .on_action(
3339                cx.listener(|pane: &mut Pane, _: &ActivateLastItem, window, cx| {
3340                    pane.activate_item(pane.items.len().saturating_sub(1), true, true, window, cx);
3341                }),
3342            )
3343            .on_action(
3344                cx.listener(|pane: &mut Pane, _: &ActivatePreviousItem, window, cx| {
3345                    pane.activate_prev_item(true, window, cx);
3346                }),
3347            )
3348            .on_action(
3349                cx.listener(|pane: &mut Pane, _: &ActivateNextItem, window, cx| {
3350                    pane.activate_next_item(true, window, cx);
3351                }),
3352            )
3353            .on_action(
3354                cx.listener(|pane, _: &SwapItemLeft, window, cx| pane.swap_item_left(window, cx)),
3355            )
3356            .on_action(
3357                cx.listener(|pane, _: &SwapItemRight, window, cx| pane.swap_item_right(window, cx)),
3358            )
3359            .on_action(cx.listener(|pane, action, window, cx| {
3360                pane.toggle_pin_tab(action, window, cx);
3361            }))
3362            .when(PreviewTabsSettings::get_global(cx).enabled, |this| {
3363                this.on_action(cx.listener(|pane: &mut Pane, _: &TogglePreviewTab, _, cx| {
3364                    if let Some(active_item_id) = pane.active_item().map(|i| i.item_id()) {
3365                        if pane.is_active_preview_item(active_item_id) {
3366                            pane.set_preview_item_id(None, cx);
3367                        } else {
3368                            pane.set_preview_item_id(Some(active_item_id), cx);
3369                        }
3370                    }
3371                }))
3372            })
3373            .on_action(
3374                cx.listener(|pane: &mut Self, action: &CloseActiveItem, window, cx| {
3375                    pane.close_active_item(action, window, cx)
3376                        .detach_and_log_err(cx)
3377                }),
3378            )
3379            .on_action(
3380                cx.listener(|pane: &mut Self, action: &CloseInactiveItems, window, cx| {
3381                    pane.close_inactive_items(action, window, cx)
3382                        .detach_and_log_err(cx);
3383                }),
3384            )
3385            .on_action(
3386                cx.listener(|pane: &mut Self, action: &CloseCleanItems, window, cx| {
3387                    pane.close_clean_items(action, window, cx)
3388                        .detach_and_log_err(cx)
3389                }),
3390            )
3391            .on_action(cx.listener(
3392                |pane: &mut Self, action: &CloseItemsToTheLeft, window, cx| {
3393                    pane.close_items_to_the_left_by_id(None, action, window, cx)
3394                        .detach_and_log_err(cx)
3395                },
3396            ))
3397            .on_action(cx.listener(
3398                |pane: &mut Self, action: &CloseItemsToTheRight, window, cx| {
3399                    pane.close_items_to_the_right_by_id(None, action, window, cx)
3400                        .detach_and_log_err(cx)
3401                },
3402            ))
3403            .on_action(
3404                cx.listener(|pane: &mut Self, action: &CloseAllItems, window, cx| {
3405                    pane.close_all_items(action, window, cx)
3406                        .detach_and_log_err(cx)
3407                }),
3408            )
3409            .on_action(
3410                cx.listener(|pane: &mut Self, action: &RevealInProjectPanel, _, cx| {
3411                    let entry_id = action
3412                        .entry_id
3413                        .map(ProjectEntryId::from_proto)
3414                        .or_else(|| pane.active_item()?.project_entry_ids(cx).first().copied());
3415                    if let Some(entry_id) = entry_id {
3416                        pane.project
3417                            .update(cx, |_, cx| {
3418                                cx.emit(project::Event::RevealInProjectPanel(entry_id))
3419                            })
3420                            .ok();
3421                    }
3422                }),
3423            )
3424            .on_action(cx.listener(|_, _: &menu::Cancel, window, cx| {
3425                if cx.stop_active_drag(window) {
3426                    return;
3427                } else {
3428                    cx.propagate();
3429                }
3430            }))
3431            .when(self.active_item().is_some() && display_tab_bar, |pane| {
3432                pane.child((self.render_tab_bar.clone())(self, window, cx))
3433            })
3434            .child({
3435                let has_worktrees = project.read(cx).visible_worktrees(cx).next().is_some();
3436                // main content
3437                div()
3438                    .flex_1()
3439                    .relative()
3440                    .group("")
3441                    .overflow_hidden()
3442                    .on_drag_move::<DraggedTab>(cx.listener(Self::handle_drag_move))
3443                    .on_drag_move::<DraggedSelection>(cx.listener(Self::handle_drag_move))
3444                    .when(is_local, |div| {
3445                        div.on_drag_move::<ExternalPaths>(cx.listener(Self::handle_drag_move))
3446                    })
3447                    .map(|div| {
3448                        if let Some(item) = self.active_item() {
3449                            div.id("pane_placeholder")
3450                                .v_flex()
3451                                .size_full()
3452                                .overflow_hidden()
3453                                .child(self.toolbar.clone())
3454                                .child(item.to_any())
3455                        } else {
3456                            let placeholder = div
3457                                .id("pane_placeholder")
3458                                .h_flex()
3459                                .size_full()
3460                                .justify_center()
3461                                .on_click(cx.listener(
3462                                    move |this, event: &ClickEvent, window, cx| {
3463                                        if event.up.click_count == 2 {
3464                                            window.dispatch_action(
3465                                                this.double_click_dispatch_action.boxed_clone(),
3466                                                cx,
3467                                            );
3468                                        }
3469                                    },
3470                                ));
3471                            if has_worktrees {
3472                                placeholder
3473                            } else {
3474                                placeholder.child(
3475                                    Label::new("Open a file or project to get started.")
3476                                        .color(Color::Muted),
3477                                )
3478                            }
3479                        }
3480                    })
3481                    .child(
3482                        // drag target
3483                        div()
3484                            .invisible()
3485                            .absolute()
3486                            .bg(cx.theme().colors().drop_target_background)
3487                            .group_drag_over::<DraggedTab>("", |style| style.visible())
3488                            .group_drag_over::<DraggedSelection>("", |style| style.visible())
3489                            .when(is_local, |div| {
3490                                div.group_drag_over::<ExternalPaths>("", |style| style.visible())
3491                            })
3492                            .when_some(self.can_drop_predicate.clone(), |this, p| {
3493                                this.can_drop(move |a, window, cx| p(a, window, cx))
3494                            })
3495                            .on_drop(cx.listener(move |this, dragged_tab, window, cx| {
3496                                this.handle_tab_drop(
3497                                    dragged_tab,
3498                                    this.active_item_index(),
3499                                    window,
3500                                    cx,
3501                                )
3502                            }))
3503                            .on_drop(cx.listener(
3504                                move |this, selection: &DraggedSelection, window, cx| {
3505                                    this.handle_dragged_selection_drop(selection, None, window, cx)
3506                                },
3507                            ))
3508                            .on_drop(cx.listener(move |this, paths, window, cx| {
3509                                this.handle_external_paths_drop(paths, window, cx)
3510                            }))
3511                            .map(|div| {
3512                                let size = DefiniteLength::Fraction(0.5);
3513                                match self.drag_split_direction {
3514                                    None => div.top_0().right_0().bottom_0().left_0(),
3515                                    Some(SplitDirection::Up) => {
3516                                        div.top_0().left_0().right_0().h(size)
3517                                    }
3518                                    Some(SplitDirection::Down) => {
3519                                        div.left_0().bottom_0().right_0().h(size)
3520                                    }
3521                                    Some(SplitDirection::Left) => {
3522                                        div.top_0().left_0().bottom_0().w(size)
3523                                    }
3524                                    Some(SplitDirection::Right) => {
3525                                        div.top_0().bottom_0().right_0().w(size)
3526                                    }
3527                                }
3528                            }),
3529                    )
3530            })
3531            .on_mouse_down(
3532                MouseButton::Navigate(NavigationDirection::Back),
3533                cx.listener(|pane, _, window, cx| {
3534                    if let Some(workspace) = pane.workspace.upgrade() {
3535                        let pane = cx.entity().downgrade();
3536                        window.defer(cx, move |window, cx| {
3537                            workspace.update(cx, |workspace, cx| {
3538                                workspace.go_back(pane, window, cx).detach_and_log_err(cx)
3539                            })
3540                        })
3541                    }
3542                }),
3543            )
3544            .on_mouse_down(
3545                MouseButton::Navigate(NavigationDirection::Forward),
3546                cx.listener(|pane, _, window, cx| {
3547                    if let Some(workspace) = pane.workspace.upgrade() {
3548                        let pane = cx.entity().downgrade();
3549                        window.defer(cx, move |window, cx| {
3550                            workspace.update(cx, |workspace, cx| {
3551                                workspace
3552                                    .go_forward(pane, window, cx)
3553                                    .detach_and_log_err(cx)
3554                            })
3555                        })
3556                    }
3557                }),
3558            )
3559    }
3560}
3561
3562impl ItemNavHistory {
3563    pub fn push<D: 'static + Send + Any>(&mut self, data: Option<D>, cx: &mut App) {
3564        if self
3565            .item
3566            .upgrade()
3567            .is_some_and(|item| item.include_in_nav_history())
3568        {
3569            self.history
3570                .push(data, self.item.clone(), self.is_preview, cx);
3571        }
3572    }
3573
3574    pub fn pop_backward(&mut self, cx: &mut App) -> Option<NavigationEntry> {
3575        self.history.pop(NavigationMode::GoingBack, cx)
3576    }
3577
3578    pub fn pop_forward(&mut self, cx: &mut App) -> Option<NavigationEntry> {
3579        self.history.pop(NavigationMode::GoingForward, cx)
3580    }
3581}
3582
3583impl NavHistory {
3584    pub fn for_each_entry(
3585        &self,
3586        cx: &App,
3587        mut f: impl FnMut(&NavigationEntry, (ProjectPath, Option<PathBuf>)),
3588    ) {
3589        let borrowed_history = self.0.lock();
3590        borrowed_history
3591            .forward_stack
3592            .iter()
3593            .chain(borrowed_history.backward_stack.iter())
3594            .chain(borrowed_history.closed_stack.iter())
3595            .for_each(|entry| {
3596                if let Some(project_and_abs_path) =
3597                    borrowed_history.paths_by_item.get(&entry.item.id())
3598                {
3599                    f(entry, project_and_abs_path.clone());
3600                } else if let Some(item) = entry.item.upgrade() {
3601                    if let Some(path) = item.project_path(cx) {
3602                        f(entry, (path, None));
3603                    }
3604                }
3605            })
3606    }
3607
3608    pub fn set_mode(&mut self, mode: NavigationMode) {
3609        self.0.lock().mode = mode;
3610    }
3611
3612    pub fn mode(&self) -> NavigationMode {
3613        self.0.lock().mode
3614    }
3615
3616    pub fn disable(&mut self) {
3617        self.0.lock().mode = NavigationMode::Disabled;
3618    }
3619
3620    pub fn enable(&mut self) {
3621        self.0.lock().mode = NavigationMode::Normal;
3622    }
3623
3624    pub fn pop(&mut self, mode: NavigationMode, cx: &mut App) -> Option<NavigationEntry> {
3625        let mut state = self.0.lock();
3626        let entry = match mode {
3627            NavigationMode::Normal | NavigationMode::Disabled | NavigationMode::ClosingItem => {
3628                return None;
3629            }
3630            NavigationMode::GoingBack => &mut state.backward_stack,
3631            NavigationMode::GoingForward => &mut state.forward_stack,
3632            NavigationMode::ReopeningClosedItem => &mut state.closed_stack,
3633        }
3634        .pop_back();
3635        if entry.is_some() {
3636            state.did_update(cx);
3637        }
3638        entry
3639    }
3640
3641    pub fn push<D: 'static + Send + Any>(
3642        &mut self,
3643        data: Option<D>,
3644        item: Arc<dyn WeakItemHandle>,
3645        is_preview: bool,
3646        cx: &mut App,
3647    ) {
3648        let state = &mut *self.0.lock();
3649        match state.mode {
3650            NavigationMode::Disabled => {}
3651            NavigationMode::Normal | NavigationMode::ReopeningClosedItem => {
3652                if state.backward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
3653                    state.backward_stack.pop_front();
3654                }
3655                state.backward_stack.push_back(NavigationEntry {
3656                    item,
3657                    data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
3658                    timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
3659                    is_preview,
3660                });
3661                state.forward_stack.clear();
3662            }
3663            NavigationMode::GoingBack => {
3664                if state.forward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
3665                    state.forward_stack.pop_front();
3666                }
3667                state.forward_stack.push_back(NavigationEntry {
3668                    item,
3669                    data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
3670                    timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
3671                    is_preview,
3672                });
3673            }
3674            NavigationMode::GoingForward => {
3675                if state.backward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
3676                    state.backward_stack.pop_front();
3677                }
3678                state.backward_stack.push_back(NavigationEntry {
3679                    item,
3680                    data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
3681                    timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
3682                    is_preview,
3683                });
3684            }
3685            NavigationMode::ClosingItem => {
3686                if state.closed_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
3687                    state.closed_stack.pop_front();
3688                }
3689                state.closed_stack.push_back(NavigationEntry {
3690                    item,
3691                    data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
3692                    timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
3693                    is_preview,
3694                });
3695            }
3696        }
3697        state.did_update(cx);
3698    }
3699
3700    pub fn remove_item(&mut self, item_id: EntityId) {
3701        let mut state = self.0.lock();
3702        state.paths_by_item.remove(&item_id);
3703        state
3704            .backward_stack
3705            .retain(|entry| entry.item.id() != item_id);
3706        state
3707            .forward_stack
3708            .retain(|entry| entry.item.id() != item_id);
3709        state
3710            .closed_stack
3711            .retain(|entry| entry.item.id() != item_id);
3712    }
3713
3714    pub fn path_for_item(&self, item_id: EntityId) -> Option<(ProjectPath, Option<PathBuf>)> {
3715        self.0.lock().paths_by_item.get(&item_id).cloned()
3716    }
3717}
3718
3719impl NavHistoryState {
3720    pub fn did_update(&self, cx: &mut App) {
3721        if let Some(pane) = self.pane.upgrade() {
3722            cx.defer(move |cx| {
3723                pane.update(cx, |pane, cx| pane.history_updated(cx));
3724            });
3725        }
3726    }
3727}
3728
3729fn dirty_message_for(buffer_path: Option<ProjectPath>) -> String {
3730    let path = buffer_path
3731        .as_ref()
3732        .and_then(|p| {
3733            p.path
3734                .to_str()
3735                .and_then(|s| if s.is_empty() { None } else { Some(s) })
3736        })
3737        .unwrap_or("This buffer");
3738    let path = truncate_and_remove_front(path, 80);
3739    format!("{path} contains unsaved edits. Do you want to save it?")
3740}
3741
3742pub fn tab_details(items: &[Box<dyn ItemHandle>], _window: &Window, cx: &App) -> Vec<usize> {
3743    let mut tab_details = items.iter().map(|_| 0).collect::<Vec<_>>();
3744    let mut tab_descriptions = HashMap::default();
3745    let mut done = false;
3746    while !done {
3747        done = true;
3748
3749        // Store item indices by their tab description.
3750        for (ix, (item, detail)) in items.iter().zip(&tab_details).enumerate() {
3751            let description = item.tab_content_text(*detail, cx);
3752            if *detail == 0 || description != item.tab_content_text(detail - 1, cx) {
3753                tab_descriptions
3754                    .entry(description)
3755                    .or_insert(Vec::new())
3756                    .push(ix);
3757            }
3758        }
3759
3760        // If two or more items have the same tab description, increase their level
3761        // of detail and try again.
3762        for (_, item_ixs) in tab_descriptions.drain() {
3763            if item_ixs.len() > 1 {
3764                done = false;
3765                for ix in item_ixs {
3766                    tab_details[ix] += 1;
3767                }
3768            }
3769        }
3770    }
3771
3772    tab_details
3773}
3774
3775pub fn render_item_indicator(item: Box<dyn ItemHandle>, cx: &App) -> Option<Indicator> {
3776    maybe!({
3777        let indicator_color = match (item.has_conflict(cx), item.is_dirty(cx)) {
3778            (true, _) => Color::Warning,
3779            (_, true) => Color::Accent,
3780            (false, false) => return None,
3781        };
3782
3783        Some(Indicator::dot().color(indicator_color))
3784    })
3785}
3786
3787impl Render for DraggedTab {
3788    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
3789        let ui_font = ThemeSettings::get_global(cx).ui_font.clone();
3790        let label = self.item.tab_content(
3791            TabContentParams {
3792                detail: Some(self.detail),
3793                selected: false,
3794                preview: false,
3795                deemphasized: false,
3796            },
3797            window,
3798            cx,
3799        );
3800        Tab::new("")
3801            .toggle_state(self.is_active)
3802            .child(label)
3803            .render(window, cx)
3804            .font(ui_font)
3805    }
3806}
3807
3808#[cfg(test)]
3809mod tests {
3810    use std::num::NonZero;
3811
3812    use super::*;
3813    use crate::item::test::{TestItem, TestProjectItem};
3814    use gpui::{TestAppContext, VisualTestContext};
3815    use project::FakeFs;
3816    use settings::SettingsStore;
3817    use theme::LoadThemes;
3818    use util::TryFutureExt;
3819
3820    #[gpui::test]
3821    async fn test_add_item_capped_to_max_tabs(cx: &mut TestAppContext) {
3822        init_test(cx);
3823        let fs = FakeFs::new(cx.executor());
3824
3825        let project = Project::test(fs, None, cx).await;
3826        let (workspace, cx) =
3827            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
3828        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
3829
3830        for i in 0..7 {
3831            add_labeled_item(&pane, format!("{}", i).as_str(), false, cx);
3832        }
3833        set_max_tabs(cx, Some(5));
3834        add_labeled_item(&pane, "7", false, cx);
3835        // Remove items to respect the max tab cap.
3836        assert_item_labels(&pane, ["3", "4", "5", "6", "7*"], cx);
3837        pane.update_in(cx, |pane, window, cx| {
3838            pane.activate_item(0, false, false, window, cx);
3839        });
3840        add_labeled_item(&pane, "X", false, cx);
3841        // Respect activation order.
3842        assert_item_labels(&pane, ["3", "X*", "5", "6", "7"], cx);
3843
3844        for i in 0..7 {
3845            add_labeled_item(&pane, format!("D{}", i).as_str(), true, cx);
3846        }
3847        // Keeps dirty items, even over max tab cap.
3848        assert_item_labels(
3849            &pane,
3850            ["D0^", "D1^", "D2^", "D3^", "D4^", "D5^", "D6*^"],
3851            cx,
3852        );
3853
3854        set_max_tabs(cx, None);
3855        for i in 0..7 {
3856            add_labeled_item(&pane, format!("N{}", i).as_str(), false, cx);
3857        }
3858        // No cap when max tabs is None.
3859        assert_item_labels(
3860            &pane,
3861            [
3862                "D0^", "D1^", "D2^", "D3^", "D4^", "D5^", "D6^", "N0", "N1", "N2", "N3", "N4",
3863                "N5", "N6*",
3864            ],
3865            cx,
3866        );
3867    }
3868
3869    #[gpui::test]
3870    async fn test_allow_pinning_dirty_item_at_max_tabs(cx: &mut TestAppContext) {
3871        init_test(cx);
3872        let fs = FakeFs::new(cx.executor());
3873
3874        let project = Project::test(fs, None, cx).await;
3875        let (workspace, cx) =
3876            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
3877        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
3878
3879        set_max_tabs(cx, Some(1));
3880        let item_a = add_labeled_item(&pane, "A", true, cx);
3881
3882        pane.update_in(cx, |pane, window, cx| {
3883            let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
3884            pane.pin_tab_at(ix, window, cx);
3885        });
3886        assert_item_labels(&pane, ["A*^!"], cx);
3887    }
3888
3889    #[gpui::test]
3890    async fn test_allow_pinning_non_dirty_item_at_max_tabs(cx: &mut TestAppContext) {
3891        init_test(cx);
3892        let fs = FakeFs::new(cx.executor());
3893
3894        let project = Project::test(fs, None, cx).await;
3895        let (workspace, cx) =
3896            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
3897        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
3898
3899        set_max_tabs(cx, Some(1));
3900        let item_a = add_labeled_item(&pane, "A", false, cx);
3901
3902        pane.update_in(cx, |pane, window, cx| {
3903            let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
3904            pane.pin_tab_at(ix, window, cx);
3905        });
3906        assert_item_labels(&pane, ["A*!"], cx);
3907    }
3908
3909    #[gpui::test]
3910    async fn test_pin_tabs_incrementally_at_max_capacity(cx: &mut TestAppContext) {
3911        init_test(cx);
3912        let fs = FakeFs::new(cx.executor());
3913
3914        let project = Project::test(fs, None, cx).await;
3915        let (workspace, cx) =
3916            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
3917        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
3918
3919        set_max_tabs(cx, Some(3));
3920
3921        let item_a = add_labeled_item(&pane, "A", false, cx);
3922        assert_item_labels(&pane, ["A*"], cx);
3923
3924        pane.update_in(cx, |pane, window, cx| {
3925            let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
3926            pane.pin_tab_at(ix, window, cx);
3927        });
3928        assert_item_labels(&pane, ["A*!"], cx);
3929
3930        let item_b = add_labeled_item(&pane, "B", false, cx);
3931        assert_item_labels(&pane, ["A!", "B*"], cx);
3932
3933        pane.update_in(cx, |pane, window, cx| {
3934            let ix = pane.index_for_item_id(item_b.item_id()).unwrap();
3935            pane.pin_tab_at(ix, window, cx);
3936        });
3937        assert_item_labels(&pane, ["A!", "B*!"], cx);
3938
3939        let item_c = add_labeled_item(&pane, "C", false, cx);
3940        assert_item_labels(&pane, ["A!", "B!", "C*"], cx);
3941
3942        pane.update_in(cx, |pane, window, cx| {
3943            let ix = pane.index_for_item_id(item_c.item_id()).unwrap();
3944            pane.pin_tab_at(ix, window, cx);
3945        });
3946        assert_item_labels(&pane, ["A!", "B!", "C*!"], cx);
3947    }
3948
3949    #[gpui::test]
3950    async fn test_pin_tabs_left_to_right_after_opening_at_max_capacity(cx: &mut TestAppContext) {
3951        init_test(cx);
3952        let fs = FakeFs::new(cx.executor());
3953
3954        let project = Project::test(fs, None, cx).await;
3955        let (workspace, cx) =
3956            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
3957        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
3958
3959        set_max_tabs(cx, Some(3));
3960
3961        let item_a = add_labeled_item(&pane, "A", false, cx);
3962        assert_item_labels(&pane, ["A*"], cx);
3963
3964        let item_b = add_labeled_item(&pane, "B", false, cx);
3965        assert_item_labels(&pane, ["A", "B*"], cx);
3966
3967        let item_c = add_labeled_item(&pane, "C", false, cx);
3968        assert_item_labels(&pane, ["A", "B", "C*"], cx);
3969
3970        pane.update_in(cx, |pane, window, cx| {
3971            let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
3972            pane.pin_tab_at(ix, window, cx);
3973        });
3974        assert_item_labels(&pane, ["A!", "B", "C*"], cx);
3975
3976        pane.update_in(cx, |pane, window, cx| {
3977            let ix = pane.index_for_item_id(item_b.item_id()).unwrap();
3978            pane.pin_tab_at(ix, window, cx);
3979        });
3980        assert_item_labels(&pane, ["A!", "B!", "C*"], cx);
3981
3982        pane.update_in(cx, |pane, window, cx| {
3983            let ix = pane.index_for_item_id(item_c.item_id()).unwrap();
3984            pane.pin_tab_at(ix, window, cx);
3985        });
3986        assert_item_labels(&pane, ["A!", "B!", "C*!"], cx);
3987    }
3988
3989    #[gpui::test]
3990    async fn test_pin_tabs_right_to_left_after_opening_at_max_capacity(cx: &mut TestAppContext) {
3991        init_test(cx);
3992        let fs = FakeFs::new(cx.executor());
3993
3994        let project = Project::test(fs, None, cx).await;
3995        let (workspace, cx) =
3996            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
3997        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
3998
3999        set_max_tabs(cx, Some(3));
4000
4001        let item_a = add_labeled_item(&pane, "A", false, cx);
4002        assert_item_labels(&pane, ["A*"], cx);
4003
4004        let item_b = add_labeled_item(&pane, "B", false, cx);
4005        assert_item_labels(&pane, ["A", "B*"], cx);
4006
4007        let item_c = add_labeled_item(&pane, "C", false, cx);
4008        assert_item_labels(&pane, ["A", "B", "C*"], cx);
4009
4010        pane.update_in(cx, |pane, window, cx| {
4011            let ix = pane.index_for_item_id(item_c.item_id()).unwrap();
4012            pane.pin_tab_at(ix, window, cx);
4013        });
4014        assert_item_labels(&pane, ["C*!", "A", "B"], cx);
4015
4016        pane.update_in(cx, |pane, window, cx| {
4017            let ix = pane.index_for_item_id(item_b.item_id()).unwrap();
4018            pane.pin_tab_at(ix, window, cx);
4019        });
4020        assert_item_labels(&pane, ["C*!", "B!", "A"], cx);
4021
4022        pane.update_in(cx, |pane, window, cx| {
4023            let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4024            pane.pin_tab_at(ix, window, cx);
4025        });
4026        assert_item_labels(&pane, ["C*!", "B!", "A!"], cx);
4027    }
4028
4029    #[gpui::test]
4030    async fn test_pinned_tabs_never_closed_at_max_tabs(cx: &mut TestAppContext) {
4031        init_test(cx);
4032        let fs = FakeFs::new(cx.executor());
4033
4034        let project = Project::test(fs, None, cx).await;
4035        let (workspace, cx) =
4036            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4037        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4038
4039        let item_a = add_labeled_item(&pane, "A", false, cx);
4040        pane.update_in(cx, |pane, window, cx| {
4041            let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4042            pane.pin_tab_at(ix, window, cx);
4043        });
4044
4045        let item_b = add_labeled_item(&pane, "B", false, cx);
4046        pane.update_in(cx, |pane, window, cx| {
4047            let ix = pane.index_for_item_id(item_b.item_id()).unwrap();
4048            pane.pin_tab_at(ix, window, cx);
4049        });
4050
4051        add_labeled_item(&pane, "C", false, cx);
4052        add_labeled_item(&pane, "D", false, cx);
4053        add_labeled_item(&pane, "E", false, cx);
4054        assert_item_labels(&pane, ["A!", "B!", "C", "D", "E*"], cx);
4055
4056        set_max_tabs(cx, Some(3));
4057        add_labeled_item(&pane, "F", false, cx);
4058        assert_item_labels(&pane, ["A!", "B!", "F*"], cx);
4059
4060        add_labeled_item(&pane, "G", false, cx);
4061        assert_item_labels(&pane, ["A!", "B!", "G*"], cx);
4062
4063        add_labeled_item(&pane, "H", false, cx);
4064        assert_item_labels(&pane, ["A!", "B!", "H*"], cx);
4065    }
4066
4067    #[gpui::test]
4068    async fn test_always_allows_one_unpinned_item_over_max_tabs_regardless_of_pinned_count(
4069        cx: &mut TestAppContext,
4070    ) {
4071        init_test(cx);
4072        let fs = FakeFs::new(cx.executor());
4073
4074        let project = Project::test(fs, None, cx).await;
4075        let (workspace, cx) =
4076            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4077        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4078
4079        set_max_tabs(cx, Some(3));
4080
4081        let item_a = add_labeled_item(&pane, "A", false, cx);
4082        pane.update_in(cx, |pane, window, cx| {
4083            let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4084            pane.pin_tab_at(ix, window, cx);
4085        });
4086
4087        let item_b = add_labeled_item(&pane, "B", false, cx);
4088        pane.update_in(cx, |pane, window, cx| {
4089            let ix = pane.index_for_item_id(item_b.item_id()).unwrap();
4090            pane.pin_tab_at(ix, window, cx);
4091        });
4092
4093        let item_c = add_labeled_item(&pane, "C", false, cx);
4094        pane.update_in(cx, |pane, window, cx| {
4095            let ix = pane.index_for_item_id(item_c.item_id()).unwrap();
4096            pane.pin_tab_at(ix, window, cx);
4097        });
4098
4099        assert_item_labels(&pane, ["A!", "B!", "C*!"], cx);
4100
4101        let item_d = add_labeled_item(&pane, "D", false, cx);
4102        assert_item_labels(&pane, ["A!", "B!", "C!", "D*"], cx);
4103
4104        pane.update_in(cx, |pane, window, cx| {
4105            let ix = pane.index_for_item_id(item_d.item_id()).unwrap();
4106            pane.pin_tab_at(ix, window, cx);
4107        });
4108        assert_item_labels(&pane, ["A!", "B!", "C!", "D*!"], cx);
4109
4110        add_labeled_item(&pane, "E", false, cx);
4111        assert_item_labels(&pane, ["A!", "B!", "C!", "D!", "E*"], cx);
4112
4113        add_labeled_item(&pane, "F", false, cx);
4114        assert_item_labels(&pane, ["A!", "B!", "C!", "D!", "F*"], cx);
4115    }
4116
4117    #[gpui::test]
4118    async fn test_can_open_one_item_when_all_tabs_are_dirty_at_max(cx: &mut TestAppContext) {
4119        init_test(cx);
4120        let fs = FakeFs::new(cx.executor());
4121
4122        let project = Project::test(fs, None, cx).await;
4123        let (workspace, cx) =
4124            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4125        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4126
4127        set_max_tabs(cx, Some(3));
4128
4129        add_labeled_item(&pane, "A", true, cx);
4130        assert_item_labels(&pane, ["A*^"], cx);
4131
4132        add_labeled_item(&pane, "B", true, cx);
4133        assert_item_labels(&pane, ["A^", "B*^"], cx);
4134
4135        add_labeled_item(&pane, "C", true, cx);
4136        assert_item_labels(&pane, ["A^", "B^", "C*^"], cx);
4137
4138        add_labeled_item(&pane, "D", false, cx);
4139        assert_item_labels(&pane, ["A^", "B^", "C^", "D*"], cx);
4140
4141        add_labeled_item(&pane, "E", false, cx);
4142        assert_item_labels(&pane, ["A^", "B^", "C^", "E*"], cx);
4143
4144        add_labeled_item(&pane, "F", false, cx);
4145        assert_item_labels(&pane, ["A^", "B^", "C^", "F*"], cx);
4146
4147        add_labeled_item(&pane, "G", true, cx);
4148        assert_item_labels(&pane, ["A^", "B^", "C^", "G*^"], cx);
4149    }
4150
4151    #[gpui::test]
4152    async fn test_toggle_pin_tab(cx: &mut TestAppContext) {
4153        init_test(cx);
4154        let fs = FakeFs::new(cx.executor());
4155
4156        let project = Project::test(fs, None, cx).await;
4157        let (workspace, cx) =
4158            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4159        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4160
4161        set_labeled_items(&pane, ["A", "B*", "C"], cx);
4162        assert_item_labels(&pane, ["A", "B*", "C"], cx);
4163
4164        pane.update_in(cx, |pane, window, cx| {
4165            pane.toggle_pin_tab(&TogglePinTab, window, cx);
4166        });
4167        assert_item_labels(&pane, ["B*!", "A", "C"], cx);
4168
4169        pane.update_in(cx, |pane, window, cx| {
4170            pane.toggle_pin_tab(&TogglePinTab, window, cx);
4171        });
4172        assert_item_labels(&pane, ["B*", "A", "C"], cx);
4173    }
4174
4175    #[gpui::test]
4176    async fn test_pinning_active_tab_without_position_change_maintains_focus(
4177        cx: &mut TestAppContext,
4178    ) {
4179        init_test(cx);
4180        let fs = FakeFs::new(cx.executor());
4181
4182        let project = Project::test(fs, None, cx).await;
4183        let (workspace, cx) =
4184            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4185        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4186
4187        // Add A
4188        let item_a = add_labeled_item(&pane, "A", false, cx);
4189        assert_item_labels(&pane, ["A*"], cx);
4190
4191        // Add B
4192        add_labeled_item(&pane, "B", false, cx);
4193        assert_item_labels(&pane, ["A", "B*"], cx);
4194
4195        // Activate A again
4196        pane.update_in(cx, |pane, window, cx| {
4197            let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4198            pane.activate_item(ix, true, true, window, cx);
4199        });
4200        assert_item_labels(&pane, ["A*", "B"], cx);
4201
4202        // Pin A - remains active
4203        pane.update_in(cx, |pane, window, cx| {
4204            let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4205            pane.pin_tab_at(ix, window, cx);
4206        });
4207        assert_item_labels(&pane, ["A*!", "B"], cx);
4208
4209        // Unpin A - remain active
4210        pane.update_in(cx, |pane, window, cx| {
4211            let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4212            pane.unpin_tab_at(ix, window, cx);
4213        });
4214        assert_item_labels(&pane, ["A*", "B"], cx);
4215    }
4216
4217    #[gpui::test]
4218    async fn test_pinning_active_tab_with_position_change_maintains_focus(cx: &mut TestAppContext) {
4219        init_test(cx);
4220        let fs = FakeFs::new(cx.executor());
4221
4222        let project = Project::test(fs, None, cx).await;
4223        let (workspace, cx) =
4224            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4225        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4226
4227        // Add A, B, C
4228        add_labeled_item(&pane, "A", false, cx);
4229        add_labeled_item(&pane, "B", false, cx);
4230        let item_c = add_labeled_item(&pane, "C", false, cx);
4231        assert_item_labels(&pane, ["A", "B", "C*"], cx);
4232
4233        // Pin C - moves to pinned area, remains active
4234        pane.update_in(cx, |pane, window, cx| {
4235            let ix = pane.index_for_item_id(item_c.item_id()).unwrap();
4236            pane.pin_tab_at(ix, window, cx);
4237        });
4238        assert_item_labels(&pane, ["C*!", "A", "B"], cx);
4239
4240        // Unpin C - moves after pinned area, remains active
4241        pane.update_in(cx, |pane, window, cx| {
4242            let ix = pane.index_for_item_id(item_c.item_id()).unwrap();
4243            pane.unpin_tab_at(ix, window, cx);
4244        });
4245        assert_item_labels(&pane, ["C*", "A", "B"], cx);
4246    }
4247
4248    #[gpui::test]
4249    async fn test_pinning_inactive_tab_without_position_change_preserves_existing_focus(
4250        cx: &mut TestAppContext,
4251    ) {
4252        init_test(cx);
4253        let fs = FakeFs::new(cx.executor());
4254
4255        let project = Project::test(fs, None, cx).await;
4256        let (workspace, cx) =
4257            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4258        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4259
4260        // Add A, B
4261        let item_a = add_labeled_item(&pane, "A", false, cx);
4262        add_labeled_item(&pane, "B", false, cx);
4263        assert_item_labels(&pane, ["A", "B*"], cx);
4264
4265        // Pin A - already in pinned area, B remains active
4266        pane.update_in(cx, |pane, window, cx| {
4267            let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4268            pane.pin_tab_at(ix, window, cx);
4269        });
4270        assert_item_labels(&pane, ["A!", "B*"], cx);
4271
4272        // Unpin A - stays in place, B remains active
4273        pane.update_in(cx, |pane, window, cx| {
4274            let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4275            pane.unpin_tab_at(ix, window, cx);
4276        });
4277        assert_item_labels(&pane, ["A", "B*"], cx);
4278    }
4279
4280    #[gpui::test]
4281    async fn test_pinning_inactive_tab_with_position_change_preserves_existing_focus(
4282        cx: &mut TestAppContext,
4283    ) {
4284        init_test(cx);
4285        let fs = FakeFs::new(cx.executor());
4286
4287        let project = Project::test(fs, None, cx).await;
4288        let (workspace, cx) =
4289            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4290        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4291
4292        // Add A, B, C
4293        add_labeled_item(&pane, "A", false, cx);
4294        let item_b = add_labeled_item(&pane, "B", false, cx);
4295        let item_c = add_labeled_item(&pane, "C", false, cx);
4296        assert_item_labels(&pane, ["A", "B", "C*"], cx);
4297
4298        // Activate B
4299        pane.update_in(cx, |pane, window, cx| {
4300            let ix = pane.index_for_item_id(item_b.item_id()).unwrap();
4301            pane.activate_item(ix, true, true, window, cx);
4302        });
4303        assert_item_labels(&pane, ["A", "B*", "C"], cx);
4304
4305        // Pin C - moves to pinned area, B remains active
4306        pane.update_in(cx, |pane, window, cx| {
4307            let ix = pane.index_for_item_id(item_c.item_id()).unwrap();
4308            pane.pin_tab_at(ix, window, cx);
4309        });
4310        assert_item_labels(&pane, ["C!", "A", "B*"], cx);
4311
4312        // Unpin C - moves after pinned area, B remains active
4313        pane.update_in(cx, |pane, window, cx| {
4314            let ix = pane.index_for_item_id(item_c.item_id()).unwrap();
4315            pane.unpin_tab_at(ix, window, cx);
4316        });
4317        assert_item_labels(&pane, ["C", "A", "B*"], cx);
4318    }
4319
4320    #[gpui::test]
4321    async fn test_drag_unpinned_tab_to_split_creates_pane_with_unpinned_tab(
4322        cx: &mut TestAppContext,
4323    ) {
4324        init_test(cx);
4325        let fs = FakeFs::new(cx.executor());
4326
4327        let project = Project::test(fs, None, cx).await;
4328        let (workspace, cx) =
4329            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4330        let pane_a = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4331
4332        // Add A, B. Pin B. Activate A
4333        let item_a = add_labeled_item(&pane_a, "A", false, cx);
4334        let item_b = add_labeled_item(&pane_a, "B", false, cx);
4335
4336        pane_a.update_in(cx, |pane, window, cx| {
4337            let ix = pane.index_for_item_id(item_b.item_id()).unwrap();
4338            pane.pin_tab_at(ix, window, cx);
4339
4340            let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4341            pane.activate_item(ix, true, true, window, cx);
4342        });
4343
4344        // Drag A to create new split
4345        pane_a.update_in(cx, |pane, window, cx| {
4346            pane.drag_split_direction = Some(SplitDirection::Right);
4347
4348            let dragged_tab = DraggedTab {
4349                pane: pane_a.clone(),
4350                item: item_a.boxed_clone(),
4351                ix: 0,
4352                detail: 0,
4353                is_active: true,
4354            };
4355            pane.handle_tab_drop(&dragged_tab, 0, window, cx);
4356        });
4357
4358        // A should be moved to new pane. B should remain pinned, A should not be pinned
4359        let (pane_a, pane_b) = workspace.read_with(cx, |workspace, _| {
4360            let panes = workspace.panes();
4361            (panes[0].clone(), panes[1].clone())
4362        });
4363        assert_item_labels(&pane_a, ["B*!"], cx);
4364        assert_item_labels(&pane_b, ["A*"], cx);
4365    }
4366
4367    #[gpui::test]
4368    async fn test_drag_pinned_tab_to_split_creates_pane_with_pinned_tab(cx: &mut TestAppContext) {
4369        init_test(cx);
4370        let fs = FakeFs::new(cx.executor());
4371
4372        let project = Project::test(fs, None, cx).await;
4373        let (workspace, cx) =
4374            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4375        let pane_a = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4376
4377        // Add A, B. Pin both. Activate A
4378        let item_a = add_labeled_item(&pane_a, "A", false, cx);
4379        let item_b = add_labeled_item(&pane_a, "B", false, cx);
4380
4381        pane_a.update_in(cx, |pane, window, cx| {
4382            let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4383            pane.pin_tab_at(ix, window, cx);
4384
4385            let ix = pane.index_for_item_id(item_b.item_id()).unwrap();
4386            pane.pin_tab_at(ix, window, cx);
4387
4388            let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4389            pane.activate_item(ix, true, true, window, cx);
4390        });
4391        assert_item_labels(&pane_a, ["A*!", "B!"], cx);
4392
4393        // Drag A to create new split
4394        pane_a.update_in(cx, |pane, window, cx| {
4395            pane.drag_split_direction = Some(SplitDirection::Right);
4396
4397            let dragged_tab = DraggedTab {
4398                pane: pane_a.clone(),
4399                item: item_a.boxed_clone(),
4400                ix: 0,
4401                detail: 0,
4402                is_active: true,
4403            };
4404            pane.handle_tab_drop(&dragged_tab, 0, window, cx);
4405        });
4406
4407        // A should be moved to new pane. Both A and B should still be pinned
4408        let (pane_a, pane_b) = workspace.read_with(cx, |workspace, _| {
4409            let panes = workspace.panes();
4410            (panes[0].clone(), panes[1].clone())
4411        });
4412        assert_item_labels(&pane_a, ["B*!"], cx);
4413        assert_item_labels(&pane_b, ["A*!"], cx);
4414    }
4415
4416    #[gpui::test]
4417    async fn test_drag_pinned_tab_into_existing_panes_pinned_region(cx: &mut TestAppContext) {
4418        init_test(cx);
4419        let fs = FakeFs::new(cx.executor());
4420
4421        let project = Project::test(fs, None, cx).await;
4422        let (workspace, cx) =
4423            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4424        let pane_a = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4425
4426        // Add A to pane A and pin
4427        let item_a = add_labeled_item(&pane_a, "A", false, cx);
4428        pane_a.update_in(cx, |pane, window, cx| {
4429            let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4430            pane.pin_tab_at(ix, window, cx);
4431        });
4432        assert_item_labels(&pane_a, ["A*!"], cx);
4433
4434        // Add B to pane B and pin
4435        let pane_b = workspace.update_in(cx, |workspace, window, cx| {
4436            workspace.split_pane(pane_a.clone(), SplitDirection::Right, window, cx)
4437        });
4438        let item_b = add_labeled_item(&pane_b, "B", false, cx);
4439        pane_b.update_in(cx, |pane, window, cx| {
4440            let ix = pane.index_for_item_id(item_b.item_id()).unwrap();
4441            pane.pin_tab_at(ix, window, cx);
4442        });
4443        assert_item_labels(&pane_b, ["B*!"], cx);
4444
4445        // Move A from pane A to pane B's pinned region
4446        pane_b.update_in(cx, |pane, window, cx| {
4447            let dragged_tab = DraggedTab {
4448                pane: pane_a.clone(),
4449                item: item_a.boxed_clone(),
4450                ix: 0,
4451                detail: 0,
4452                is_active: true,
4453            };
4454            pane.handle_tab_drop(&dragged_tab, 0, window, cx);
4455        });
4456
4457        // A should stay pinned
4458        assert_item_labels(&pane_a, [], cx);
4459        assert_item_labels(&pane_b, ["A*!", "B!"], cx);
4460    }
4461
4462    #[gpui::test]
4463    async fn test_drag_pinned_tab_into_existing_panes_unpinned_region(cx: &mut TestAppContext) {
4464        init_test(cx);
4465        let fs = FakeFs::new(cx.executor());
4466
4467        let project = Project::test(fs, None, cx).await;
4468        let (workspace, cx) =
4469            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4470        let pane_a = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4471
4472        // Add A to pane A and pin
4473        let item_a = add_labeled_item(&pane_a, "A", false, cx);
4474        pane_a.update_in(cx, |pane, window, cx| {
4475            let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4476            pane.pin_tab_at(ix, window, cx);
4477        });
4478        assert_item_labels(&pane_a, ["A*!"], cx);
4479
4480        // Create pane B with pinned item B
4481        let pane_b = workspace.update_in(cx, |workspace, window, cx| {
4482            workspace.split_pane(pane_a.clone(), SplitDirection::Right, window, cx)
4483        });
4484        let item_b = add_labeled_item(&pane_b, "B", false, cx);
4485        assert_item_labels(&pane_b, ["B*"], cx);
4486
4487        pane_b.update_in(cx, |pane, window, cx| {
4488            let ix = pane.index_for_item_id(item_b.item_id()).unwrap();
4489            pane.pin_tab_at(ix, window, cx);
4490        });
4491        assert_item_labels(&pane_b, ["B*!"], cx);
4492
4493        // Move A from pane A to pane B's unpinned region
4494        pane_b.update_in(cx, |pane, window, cx| {
4495            let dragged_tab = DraggedTab {
4496                pane: pane_a.clone(),
4497                item: item_a.boxed_clone(),
4498                ix: 0,
4499                detail: 0,
4500                is_active: true,
4501            };
4502            pane.handle_tab_drop(&dragged_tab, 1, window, cx);
4503        });
4504
4505        // A should become pinned
4506        assert_item_labels(&pane_a, [], cx);
4507        assert_item_labels(&pane_b, ["B!", "A*"], cx);
4508    }
4509
4510    #[gpui::test]
4511    async fn test_drag_pinned_tab_into_existing_panes_first_position_with_no_pinned_tabs(
4512        cx: &mut TestAppContext,
4513    ) {
4514        init_test(cx);
4515        let fs = FakeFs::new(cx.executor());
4516
4517        let project = Project::test(fs, None, cx).await;
4518        let (workspace, cx) =
4519            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4520        let pane_a = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4521
4522        // Add A to pane A and pin
4523        let item_a = add_labeled_item(&pane_a, "A", false, cx);
4524        pane_a.update_in(cx, |pane, window, cx| {
4525            let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4526            pane.pin_tab_at(ix, window, cx);
4527        });
4528        assert_item_labels(&pane_a, ["A*!"], cx);
4529
4530        // Add B to pane B
4531        let pane_b = workspace.update_in(cx, |workspace, window, cx| {
4532            workspace.split_pane(pane_a.clone(), SplitDirection::Right, window, cx)
4533        });
4534        add_labeled_item(&pane_b, "B", false, cx);
4535        assert_item_labels(&pane_b, ["B*"], cx);
4536
4537        // Move A from pane A to position 0 in pane B, indicating it should stay pinned
4538        pane_b.update_in(cx, |pane, window, cx| {
4539            let dragged_tab = DraggedTab {
4540                pane: pane_a.clone(),
4541                item: item_a.boxed_clone(),
4542                ix: 0,
4543                detail: 0,
4544                is_active: true,
4545            };
4546            pane.handle_tab_drop(&dragged_tab, 0, window, cx);
4547        });
4548
4549        // A should stay pinned
4550        assert_item_labels(&pane_a, [], cx);
4551        assert_item_labels(&pane_b, ["A*!", "B"], cx);
4552    }
4553
4554    #[gpui::test]
4555    async fn test_drag_pinned_tab_into_existing_pane_at_max_capacity_closes_unpinned_tabs(
4556        cx: &mut TestAppContext,
4557    ) {
4558        init_test(cx);
4559        let fs = FakeFs::new(cx.executor());
4560
4561        let project = Project::test(fs, None, cx).await;
4562        let (workspace, cx) =
4563            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4564        let pane_a = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4565        set_max_tabs(cx, Some(2));
4566
4567        // Add A, B to pane A. Pin both
4568        let item_a = add_labeled_item(&pane_a, "A", false, cx);
4569        let item_b = add_labeled_item(&pane_a, "B", false, cx);
4570        pane_a.update_in(cx, |pane, window, cx| {
4571            let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4572            pane.pin_tab_at(ix, window, cx);
4573
4574            let ix = pane.index_for_item_id(item_b.item_id()).unwrap();
4575            pane.pin_tab_at(ix, window, cx);
4576        });
4577        assert_item_labels(&pane_a, ["A!", "B*!"], cx);
4578
4579        // Add C, D to pane B. Pin both
4580        let pane_b = workspace.update_in(cx, |workspace, window, cx| {
4581            workspace.split_pane(pane_a.clone(), SplitDirection::Right, window, cx)
4582        });
4583        let item_c = add_labeled_item(&pane_b, "C", false, cx);
4584        let item_d = add_labeled_item(&pane_b, "D", false, cx);
4585        pane_b.update_in(cx, |pane, window, cx| {
4586            let ix = pane.index_for_item_id(item_c.item_id()).unwrap();
4587            pane.pin_tab_at(ix, window, cx);
4588
4589            let ix = pane.index_for_item_id(item_d.item_id()).unwrap();
4590            pane.pin_tab_at(ix, window, cx);
4591        });
4592        assert_item_labels(&pane_b, ["C!", "D*!"], cx);
4593
4594        // Add a third unpinned item to pane B (exceeds max tabs), but is allowed,
4595        // as we allow 1 tab over max if the others are pinned or dirty
4596        add_labeled_item(&pane_b, "E", false, cx);
4597        assert_item_labels(&pane_b, ["C!", "D!", "E*"], cx);
4598
4599        // Drag pinned A from pane A to position 0 in pane B
4600        pane_b.update_in(cx, |pane, window, cx| {
4601            let dragged_tab = DraggedTab {
4602                pane: pane_a.clone(),
4603                item: item_a.boxed_clone(),
4604                ix: 0,
4605                detail: 0,
4606                is_active: true,
4607            };
4608            pane.handle_tab_drop(&dragged_tab, 0, window, cx);
4609        });
4610
4611        // E (unpinned) should be closed, leaving 3 pinned items
4612        assert_item_labels(&pane_a, ["B*!"], cx);
4613        assert_item_labels(&pane_b, ["A*!", "C!", "D!"], cx);
4614    }
4615
4616    #[gpui::test]
4617    async fn test_drag_last_pinned_tab_to_same_position_stays_pinned(cx: &mut TestAppContext) {
4618        init_test(cx);
4619        let fs = FakeFs::new(cx.executor());
4620
4621        let project = Project::test(fs, None, cx).await;
4622        let (workspace, cx) =
4623            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4624        let pane_a = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4625
4626        // Add A to pane A and pin it
4627        let item_a = add_labeled_item(&pane_a, "A", false, cx);
4628        pane_a.update_in(cx, |pane, window, cx| {
4629            let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4630            pane.pin_tab_at(ix, window, cx);
4631        });
4632        assert_item_labels(&pane_a, ["A*!"], cx);
4633
4634        // Drag pinned A to position 1 (directly to the right) in the same pane
4635        pane_a.update_in(cx, |pane, window, cx| {
4636            let dragged_tab = DraggedTab {
4637                pane: pane_a.clone(),
4638                item: item_a.boxed_clone(),
4639                ix: 0,
4640                detail: 0,
4641                is_active: true,
4642            };
4643            pane.handle_tab_drop(&dragged_tab, 1, window, cx);
4644        });
4645
4646        // A should still be pinned and active
4647        assert_item_labels(&pane_a, ["A*!"], cx);
4648    }
4649
4650    #[gpui::test]
4651    async fn test_drag_pinned_tab_beyond_last_pinned_tab_in_same_pane_stays_pinned(
4652        cx: &mut TestAppContext,
4653    ) {
4654        init_test(cx);
4655        let fs = FakeFs::new(cx.executor());
4656
4657        let project = Project::test(fs, None, cx).await;
4658        let (workspace, cx) =
4659            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4660        let pane_a = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4661
4662        // Add A, B to pane A and pin both
4663        let item_a = add_labeled_item(&pane_a, "A", false, cx);
4664        let item_b = add_labeled_item(&pane_a, "B", false, cx);
4665        pane_a.update_in(cx, |pane, window, cx| {
4666            let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4667            pane.pin_tab_at(ix, window, cx);
4668
4669            let ix = pane.index_for_item_id(item_b.item_id()).unwrap();
4670            pane.pin_tab_at(ix, window, cx);
4671        });
4672        assert_item_labels(&pane_a, ["A!", "B*!"], cx);
4673
4674        // Drag pinned A right of B in the same pane
4675        pane_a.update_in(cx, |pane, window, cx| {
4676            let dragged_tab = DraggedTab {
4677                pane: pane_a.clone(),
4678                item: item_a.boxed_clone(),
4679                ix: 0,
4680                detail: 0,
4681                is_active: true,
4682            };
4683            pane.handle_tab_drop(&dragged_tab, 2, window, cx);
4684        });
4685
4686        // A stays pinned
4687        assert_item_labels(&pane_a, ["B!", "A*!"], cx);
4688    }
4689
4690    #[gpui::test]
4691    async fn test_drag_pinned_tab_beyond_unpinned_tab_in_same_pane_becomes_unpinned(
4692        cx: &mut TestAppContext,
4693    ) {
4694        init_test(cx);
4695        let fs = FakeFs::new(cx.executor());
4696
4697        let project = Project::test(fs, None, cx).await;
4698        let (workspace, cx) =
4699            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4700        let pane_a = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4701
4702        // Add A, B to pane A and pin A
4703        let item_a = add_labeled_item(&pane_a, "A", false, cx);
4704        add_labeled_item(&pane_a, "B", false, cx);
4705        pane_a.update_in(cx, |pane, window, cx| {
4706            let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4707            pane.pin_tab_at(ix, window, cx);
4708        });
4709        assert_item_labels(&pane_a, ["A!", "B*"], cx);
4710
4711        // Drag pinned A right of B in the same pane
4712        pane_a.update_in(cx, |pane, window, cx| {
4713            let dragged_tab = DraggedTab {
4714                pane: pane_a.clone(),
4715                item: item_a.boxed_clone(),
4716                ix: 0,
4717                detail: 0,
4718                is_active: true,
4719            };
4720            pane.handle_tab_drop(&dragged_tab, 2, window, cx);
4721        });
4722
4723        // A becomes unpinned
4724        assert_item_labels(&pane_a, ["B", "A*"], cx);
4725    }
4726
4727    #[gpui::test]
4728    async fn test_drag_unpinned_tab_in_front_of_pinned_tab_in_same_pane_becomes_pinned(
4729        cx: &mut TestAppContext,
4730    ) {
4731        init_test(cx);
4732        let fs = FakeFs::new(cx.executor());
4733
4734        let project = Project::test(fs, None, cx).await;
4735        let (workspace, cx) =
4736            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4737        let pane_a = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4738
4739        // Add A, B to pane A and pin A
4740        let item_a = add_labeled_item(&pane_a, "A", false, cx);
4741        let item_b = add_labeled_item(&pane_a, "B", false, cx);
4742        pane_a.update_in(cx, |pane, window, cx| {
4743            let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4744            pane.pin_tab_at(ix, window, cx);
4745        });
4746        assert_item_labels(&pane_a, ["A!", "B*"], cx);
4747
4748        // Drag pinned B left of A in the same pane
4749        pane_a.update_in(cx, |pane, window, cx| {
4750            let dragged_tab = DraggedTab {
4751                pane: pane_a.clone(),
4752                item: item_b.boxed_clone(),
4753                ix: 1,
4754                detail: 0,
4755                is_active: true,
4756            };
4757            pane.handle_tab_drop(&dragged_tab, 0, window, cx);
4758        });
4759
4760        // A becomes unpinned
4761        assert_item_labels(&pane_a, ["B*!", "A!"], cx);
4762    }
4763
4764    #[gpui::test]
4765    async fn test_drag_unpinned_tab_to_the_pinned_region_stays_pinned(cx: &mut TestAppContext) {
4766        init_test(cx);
4767        let fs = FakeFs::new(cx.executor());
4768
4769        let project = Project::test(fs, None, cx).await;
4770        let (workspace, cx) =
4771            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4772        let pane_a = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4773
4774        // Add A, B, C to pane A and pin A
4775        let item_a = add_labeled_item(&pane_a, "A", false, cx);
4776        add_labeled_item(&pane_a, "B", false, cx);
4777        let item_c = add_labeled_item(&pane_a, "C", false, cx);
4778        pane_a.update_in(cx, |pane, window, cx| {
4779            let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4780            pane.pin_tab_at(ix, window, cx);
4781        });
4782        assert_item_labels(&pane_a, ["A!", "B", "C*"], cx);
4783
4784        // Drag pinned C left of B in the same pane
4785        pane_a.update_in(cx, |pane, window, cx| {
4786            let dragged_tab = DraggedTab {
4787                pane: pane_a.clone(),
4788                item: item_c.boxed_clone(),
4789                ix: 2,
4790                detail: 0,
4791                is_active: true,
4792            };
4793            pane.handle_tab_drop(&dragged_tab, 1, window, cx);
4794        });
4795
4796        // A stays pinned, B and C remain unpinned
4797        assert_item_labels(&pane_a, ["A!", "C*", "B"], cx);
4798    }
4799
4800    #[gpui::test]
4801    async fn test_drag_unpinned_tab_into_existing_panes_pinned_region(cx: &mut TestAppContext) {
4802        init_test(cx);
4803        let fs = FakeFs::new(cx.executor());
4804
4805        let project = Project::test(fs, None, cx).await;
4806        let (workspace, cx) =
4807            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4808        let pane_a = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4809
4810        // Add unpinned item A to pane A
4811        let item_a = add_labeled_item(&pane_a, "A", false, cx);
4812        assert_item_labels(&pane_a, ["A*"], cx);
4813
4814        // Create pane B with pinned item B
4815        let pane_b = workspace.update_in(cx, |workspace, window, cx| {
4816            workspace.split_pane(pane_a.clone(), SplitDirection::Right, window, cx)
4817        });
4818        let item_b = add_labeled_item(&pane_b, "B", false, cx);
4819        pane_b.update_in(cx, |pane, window, cx| {
4820            let ix = pane.index_for_item_id(item_b.item_id()).unwrap();
4821            pane.pin_tab_at(ix, window, cx);
4822        });
4823        assert_item_labels(&pane_b, ["B*!"], cx);
4824
4825        // Move A from pane A to pane B's pinned region
4826        pane_b.update_in(cx, |pane, window, cx| {
4827            let dragged_tab = DraggedTab {
4828                pane: pane_a.clone(),
4829                item: item_a.boxed_clone(),
4830                ix: 0,
4831                detail: 0,
4832                is_active: true,
4833            };
4834            pane.handle_tab_drop(&dragged_tab, 0, window, cx);
4835        });
4836
4837        // A should become pinned since it was dropped in the pinned region
4838        assert_item_labels(&pane_a, [], cx);
4839        assert_item_labels(&pane_b, ["A*!", "B!"], cx);
4840    }
4841
4842    #[gpui::test]
4843    async fn test_drag_unpinned_tab_into_existing_panes_unpinned_region(cx: &mut TestAppContext) {
4844        init_test(cx);
4845        let fs = FakeFs::new(cx.executor());
4846
4847        let project = Project::test(fs, None, cx).await;
4848        let (workspace, cx) =
4849            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4850        let pane_a = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4851
4852        // Add unpinned item A to pane A
4853        let item_a = add_labeled_item(&pane_a, "A", false, cx);
4854        assert_item_labels(&pane_a, ["A*"], cx);
4855
4856        // Create pane B with one pinned item B
4857        let pane_b = workspace.update_in(cx, |workspace, window, cx| {
4858            workspace.split_pane(pane_a.clone(), SplitDirection::Right, window, cx)
4859        });
4860        let item_b = add_labeled_item(&pane_b, "B", false, cx);
4861        pane_b.update_in(cx, |pane, window, cx| {
4862            let ix = pane.index_for_item_id(item_b.item_id()).unwrap();
4863            pane.pin_tab_at(ix, window, cx);
4864        });
4865        assert_item_labels(&pane_b, ["B*!"], cx);
4866
4867        // Move A from pane A to pane B's unpinned region
4868        pane_b.update_in(cx, |pane, window, cx| {
4869            let dragged_tab = DraggedTab {
4870                pane: pane_a.clone(),
4871                item: item_a.boxed_clone(),
4872                ix: 0,
4873                detail: 0,
4874                is_active: true,
4875            };
4876            pane.handle_tab_drop(&dragged_tab, 1, window, cx);
4877        });
4878
4879        // A should remain unpinned since it was dropped outside the pinned region
4880        assert_item_labels(&pane_a, [], cx);
4881        assert_item_labels(&pane_b, ["B!", "A*"], cx);
4882    }
4883
4884    #[gpui::test]
4885    async fn test_drag_pinned_tab_throughout_entire_range_of_pinned_tabs_both_directions(
4886        cx: &mut TestAppContext,
4887    ) {
4888        init_test(cx);
4889        let fs = FakeFs::new(cx.executor());
4890
4891        let project = Project::test(fs, None, cx).await;
4892        let (workspace, cx) =
4893            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4894        let pane_a = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4895
4896        // Add A, B, C and pin all
4897        let item_a = add_labeled_item(&pane_a, "A", false, cx);
4898        let item_b = add_labeled_item(&pane_a, "B", false, cx);
4899        let item_c = add_labeled_item(&pane_a, "C", false, cx);
4900        assert_item_labels(&pane_a, ["A", "B", "C*"], cx);
4901
4902        pane_a.update_in(cx, |pane, window, cx| {
4903            let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4904            pane.pin_tab_at(ix, window, cx);
4905
4906            let ix = pane.index_for_item_id(item_b.item_id()).unwrap();
4907            pane.pin_tab_at(ix, window, cx);
4908
4909            let ix = pane.index_for_item_id(item_c.item_id()).unwrap();
4910            pane.pin_tab_at(ix, window, cx);
4911        });
4912        assert_item_labels(&pane_a, ["A!", "B!", "C*!"], cx);
4913
4914        // Move A to right of B
4915        pane_a.update_in(cx, |pane, window, cx| {
4916            let dragged_tab = DraggedTab {
4917                pane: pane_a.clone(),
4918                item: item_a.boxed_clone(),
4919                ix: 0,
4920                detail: 0,
4921                is_active: true,
4922            };
4923            pane.handle_tab_drop(&dragged_tab, 1, window, cx);
4924        });
4925
4926        // A should be after B and all are pinned
4927        assert_item_labels(&pane_a, ["B!", "A*!", "C!"], cx);
4928
4929        // Move A to right of C
4930        pane_a.update_in(cx, |pane, window, cx| {
4931            let dragged_tab = DraggedTab {
4932                pane: pane_a.clone(),
4933                item: item_a.boxed_clone(),
4934                ix: 1,
4935                detail: 0,
4936                is_active: true,
4937            };
4938            pane.handle_tab_drop(&dragged_tab, 2, window, cx);
4939        });
4940
4941        // A should be after C and all are pinned
4942        assert_item_labels(&pane_a, ["B!", "C!", "A*!"], cx);
4943
4944        // Move A to left of C
4945        pane_a.update_in(cx, |pane, window, cx| {
4946            let dragged_tab = DraggedTab {
4947                pane: pane_a.clone(),
4948                item: item_a.boxed_clone(),
4949                ix: 2,
4950                detail: 0,
4951                is_active: true,
4952            };
4953            pane.handle_tab_drop(&dragged_tab, 1, window, cx);
4954        });
4955
4956        // A should be before C and all are pinned
4957        assert_item_labels(&pane_a, ["B!", "A*!", "C!"], cx);
4958
4959        // Move A to left of B
4960        pane_a.update_in(cx, |pane, window, cx| {
4961            let dragged_tab = DraggedTab {
4962                pane: pane_a.clone(),
4963                item: item_a.boxed_clone(),
4964                ix: 1,
4965                detail: 0,
4966                is_active: true,
4967            };
4968            pane.handle_tab_drop(&dragged_tab, 0, window, cx);
4969        });
4970
4971        // A should be before B and all are pinned
4972        assert_item_labels(&pane_a, ["A*!", "B!", "C!"], cx);
4973    }
4974
4975    #[gpui::test]
4976    async fn test_drag_first_tab_to_last_position(cx: &mut TestAppContext) {
4977        init_test(cx);
4978        let fs = FakeFs::new(cx.executor());
4979
4980        let project = Project::test(fs, None, cx).await;
4981        let (workspace, cx) =
4982            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4983        let pane_a = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4984
4985        // Add A, B, C
4986        let item_a = add_labeled_item(&pane_a, "A", false, cx);
4987        add_labeled_item(&pane_a, "B", false, cx);
4988        add_labeled_item(&pane_a, "C", false, cx);
4989        assert_item_labels(&pane_a, ["A", "B", "C*"], cx);
4990
4991        // Move A to the end
4992        pane_a.update_in(cx, |pane, window, cx| {
4993            let dragged_tab = DraggedTab {
4994                pane: pane_a.clone(),
4995                item: item_a.boxed_clone(),
4996                ix: 0,
4997                detail: 0,
4998                is_active: true,
4999            };
5000            pane.handle_tab_drop(&dragged_tab, 2, window, cx);
5001        });
5002
5003        // A should be at the end
5004        assert_item_labels(&pane_a, ["B", "C", "A*"], cx);
5005    }
5006
5007    #[gpui::test]
5008    async fn test_drag_last_tab_to_first_position(cx: &mut TestAppContext) {
5009        init_test(cx);
5010        let fs = FakeFs::new(cx.executor());
5011
5012        let project = Project::test(fs, None, cx).await;
5013        let (workspace, cx) =
5014            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
5015        let pane_a = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
5016
5017        // Add A, B, C
5018        add_labeled_item(&pane_a, "A", false, cx);
5019        add_labeled_item(&pane_a, "B", false, cx);
5020        let item_c = add_labeled_item(&pane_a, "C", false, cx);
5021        assert_item_labels(&pane_a, ["A", "B", "C*"], cx);
5022
5023        // Move C to the beginning
5024        pane_a.update_in(cx, |pane, window, cx| {
5025            let dragged_tab = DraggedTab {
5026                pane: pane_a.clone(),
5027                item: item_c.boxed_clone(),
5028                ix: 2,
5029                detail: 0,
5030                is_active: true,
5031            };
5032            pane.handle_tab_drop(&dragged_tab, 0, window, cx);
5033        });
5034
5035        // C should be at the beginning
5036        assert_item_labels(&pane_a, ["C*", "A", "B"], cx);
5037    }
5038
5039    #[gpui::test]
5040    async fn test_add_item_with_new_item(cx: &mut TestAppContext) {
5041        init_test(cx);
5042        let fs = FakeFs::new(cx.executor());
5043
5044        let project = Project::test(fs, None, cx).await;
5045        let (workspace, cx) =
5046            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
5047        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
5048
5049        // 1. Add with a destination index
5050        //   a. Add before the active item
5051        set_labeled_items(&pane, ["A", "B*", "C"], cx);
5052        pane.update_in(cx, |pane, window, cx| {
5053            pane.add_item(
5054                Box::new(cx.new(|cx| TestItem::new(cx).with_label("D"))),
5055                false,
5056                false,
5057                Some(0),
5058                window,
5059                cx,
5060            );
5061        });
5062        assert_item_labels(&pane, ["D*", "A", "B", "C"], cx);
5063
5064        //   b. Add after the active item
5065        set_labeled_items(&pane, ["A", "B*", "C"], cx);
5066        pane.update_in(cx, |pane, window, cx| {
5067            pane.add_item(
5068                Box::new(cx.new(|cx| TestItem::new(cx).with_label("D"))),
5069                false,
5070                false,
5071                Some(2),
5072                window,
5073                cx,
5074            );
5075        });
5076        assert_item_labels(&pane, ["A", "B", "D*", "C"], cx);
5077
5078        //   c. Add at the end of the item list (including off the length)
5079        set_labeled_items(&pane, ["A", "B*", "C"], cx);
5080        pane.update_in(cx, |pane, window, cx| {
5081            pane.add_item(
5082                Box::new(cx.new(|cx| TestItem::new(cx).with_label("D"))),
5083                false,
5084                false,
5085                Some(5),
5086                window,
5087                cx,
5088            );
5089        });
5090        assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
5091
5092        // 2. Add without a destination index
5093        //   a. Add with active item at the start of the item list
5094        set_labeled_items(&pane, ["A*", "B", "C"], cx);
5095        pane.update_in(cx, |pane, window, cx| {
5096            pane.add_item(
5097                Box::new(cx.new(|cx| TestItem::new(cx).with_label("D"))),
5098                false,
5099                false,
5100                None,
5101                window,
5102                cx,
5103            );
5104        });
5105        set_labeled_items(&pane, ["A", "D*", "B", "C"], cx);
5106
5107        //   b. Add with active item at the end of the item list
5108        set_labeled_items(&pane, ["A", "B", "C*"], cx);
5109        pane.update_in(cx, |pane, window, cx| {
5110            pane.add_item(
5111                Box::new(cx.new(|cx| TestItem::new(cx).with_label("D"))),
5112                false,
5113                false,
5114                None,
5115                window,
5116                cx,
5117            );
5118        });
5119        assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
5120    }
5121
5122    #[gpui::test]
5123    async fn test_add_item_with_existing_item(cx: &mut TestAppContext) {
5124        init_test(cx);
5125        let fs = FakeFs::new(cx.executor());
5126
5127        let project = Project::test(fs, None, cx).await;
5128        let (workspace, cx) =
5129            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
5130        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
5131
5132        // 1. Add with a destination index
5133        //   1a. Add before the active item
5134        let [_, _, _, d] = set_labeled_items(&pane, ["A", "B*", "C", "D"], cx);
5135        pane.update_in(cx, |pane, window, cx| {
5136            pane.add_item(d, false, false, Some(0), window, cx);
5137        });
5138        assert_item_labels(&pane, ["D*", "A", "B", "C"], cx);
5139
5140        //   1b. Add after the active item
5141        let [_, _, _, d] = set_labeled_items(&pane, ["A", "B*", "C", "D"], cx);
5142        pane.update_in(cx, |pane, window, cx| {
5143            pane.add_item(d, false, false, Some(2), window, cx);
5144        });
5145        assert_item_labels(&pane, ["A", "B", "D*", "C"], cx);
5146
5147        //   1c. Add at the end of the item list (including off the length)
5148        let [a, _, _, _] = set_labeled_items(&pane, ["A", "B*", "C", "D"], cx);
5149        pane.update_in(cx, |pane, window, cx| {
5150            pane.add_item(a, false, false, Some(5), window, cx);
5151        });
5152        assert_item_labels(&pane, ["B", "C", "D", "A*"], cx);
5153
5154        //   1d. Add same item to active index
5155        let [_, b, _] = set_labeled_items(&pane, ["A", "B*", "C"], cx);
5156        pane.update_in(cx, |pane, window, cx| {
5157            pane.add_item(b, false, false, Some(1), window, cx);
5158        });
5159        assert_item_labels(&pane, ["A", "B*", "C"], cx);
5160
5161        //   1e. Add item to index after same item in last position
5162        let [_, _, c] = set_labeled_items(&pane, ["A", "B*", "C"], cx);
5163        pane.update_in(cx, |pane, window, cx| {
5164            pane.add_item(c, false, false, Some(2), window, cx);
5165        });
5166        assert_item_labels(&pane, ["A", "B", "C*"], cx);
5167
5168        // 2. Add without a destination index
5169        //   2a. Add with active item at the start of the item list
5170        let [_, _, _, d] = set_labeled_items(&pane, ["A*", "B", "C", "D"], cx);
5171        pane.update_in(cx, |pane, window, cx| {
5172            pane.add_item(d, false, false, None, window, cx);
5173        });
5174        assert_item_labels(&pane, ["A", "D*", "B", "C"], cx);
5175
5176        //   2b. Add with active item at the end of the item list
5177        let [a, _, _, _] = set_labeled_items(&pane, ["A", "B", "C", "D*"], cx);
5178        pane.update_in(cx, |pane, window, cx| {
5179            pane.add_item(a, false, false, None, window, cx);
5180        });
5181        assert_item_labels(&pane, ["B", "C", "D", "A*"], cx);
5182
5183        //   2c. Add active item to active item at end of list
5184        let [_, _, c] = set_labeled_items(&pane, ["A", "B", "C*"], cx);
5185        pane.update_in(cx, |pane, window, cx| {
5186            pane.add_item(c, false, false, None, window, cx);
5187        });
5188        assert_item_labels(&pane, ["A", "B", "C*"], cx);
5189
5190        //   2d. Add active item to active item at start of list
5191        let [a, _, _] = set_labeled_items(&pane, ["A*", "B", "C"], cx);
5192        pane.update_in(cx, |pane, window, cx| {
5193            pane.add_item(a, false, false, None, window, cx);
5194        });
5195        assert_item_labels(&pane, ["A*", "B", "C"], cx);
5196    }
5197
5198    #[gpui::test]
5199    async fn test_add_item_with_same_project_entries(cx: &mut TestAppContext) {
5200        init_test(cx);
5201        let fs = FakeFs::new(cx.executor());
5202
5203        let project = Project::test(fs, None, cx).await;
5204        let (workspace, cx) =
5205            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
5206        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
5207
5208        // singleton view
5209        pane.update_in(cx, |pane, window, cx| {
5210            pane.add_item(
5211                Box::new(cx.new(|cx| {
5212                    TestItem::new(cx)
5213                        .with_singleton(true)
5214                        .with_label("buffer 1")
5215                        .with_project_items(&[TestProjectItem::new(1, "one.txt", cx)])
5216                })),
5217                false,
5218                false,
5219                None,
5220                window,
5221                cx,
5222            );
5223        });
5224        assert_item_labels(&pane, ["buffer 1*"], cx);
5225
5226        // new singleton view with the same project entry
5227        pane.update_in(cx, |pane, window, cx| {
5228            pane.add_item(
5229                Box::new(cx.new(|cx| {
5230                    TestItem::new(cx)
5231                        .with_singleton(true)
5232                        .with_label("buffer 1")
5233                        .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
5234                })),
5235                false,
5236                false,
5237                None,
5238                window,
5239                cx,
5240            );
5241        });
5242        assert_item_labels(&pane, ["buffer 1*"], cx);
5243
5244        // new singleton view with different project entry
5245        pane.update_in(cx, |pane, window, cx| {
5246            pane.add_item(
5247                Box::new(cx.new(|cx| {
5248                    TestItem::new(cx)
5249                        .with_singleton(true)
5250                        .with_label("buffer 2")
5251                        .with_project_items(&[TestProjectItem::new(2, "2.txt", cx)])
5252                })),
5253                false,
5254                false,
5255                None,
5256                window,
5257                cx,
5258            );
5259        });
5260        assert_item_labels(&pane, ["buffer 1", "buffer 2*"], cx);
5261
5262        // new multibuffer view with the same project entry
5263        pane.update_in(cx, |pane, window, cx| {
5264            pane.add_item(
5265                Box::new(cx.new(|cx| {
5266                    TestItem::new(cx)
5267                        .with_singleton(false)
5268                        .with_label("multibuffer 1")
5269                        .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
5270                })),
5271                false,
5272                false,
5273                None,
5274                window,
5275                cx,
5276            );
5277        });
5278        assert_item_labels(&pane, ["buffer 1", "buffer 2", "multibuffer 1*"], cx);
5279
5280        // another multibuffer view with the same project entry
5281        pane.update_in(cx, |pane, window, cx| {
5282            pane.add_item(
5283                Box::new(cx.new(|cx| {
5284                    TestItem::new(cx)
5285                        .with_singleton(false)
5286                        .with_label("multibuffer 1b")
5287                        .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
5288                })),
5289                false,
5290                false,
5291                None,
5292                window,
5293                cx,
5294            );
5295        });
5296        assert_item_labels(
5297            &pane,
5298            ["buffer 1", "buffer 2", "multibuffer 1", "multibuffer 1b*"],
5299            cx,
5300        );
5301    }
5302
5303    #[gpui::test]
5304    async fn test_remove_item_ordering_history(cx: &mut TestAppContext) {
5305        init_test(cx);
5306        let fs = FakeFs::new(cx.executor());
5307
5308        let project = Project::test(fs, None, cx).await;
5309        let (workspace, cx) =
5310            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
5311        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
5312
5313        add_labeled_item(&pane, "A", false, cx);
5314        add_labeled_item(&pane, "B", false, cx);
5315        add_labeled_item(&pane, "C", false, cx);
5316        add_labeled_item(&pane, "D", false, cx);
5317        assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
5318
5319        pane.update_in(cx, |pane, window, cx| {
5320            pane.activate_item(1, false, false, window, cx)
5321        });
5322        add_labeled_item(&pane, "1", false, cx);
5323        assert_item_labels(&pane, ["A", "B", "1*", "C", "D"], cx);
5324
5325        pane.update_in(cx, |pane, window, cx| {
5326            pane.close_active_item(
5327                &CloseActiveItem {
5328                    save_intent: None,
5329                    close_pinned: false,
5330                },
5331                window,
5332                cx,
5333            )
5334        })
5335        .await
5336        .unwrap();
5337        assert_item_labels(&pane, ["A", "B*", "C", "D"], cx);
5338
5339        pane.update_in(cx, |pane, window, cx| {
5340            pane.activate_item(3, false, false, window, cx)
5341        });
5342        assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
5343
5344        pane.update_in(cx, |pane, window, cx| {
5345            pane.close_active_item(
5346                &CloseActiveItem {
5347                    save_intent: None,
5348                    close_pinned: false,
5349                },
5350                window,
5351                cx,
5352            )
5353        })
5354        .await
5355        .unwrap();
5356        assert_item_labels(&pane, ["A", "B*", "C"], cx);
5357
5358        pane.update_in(cx, |pane, window, cx| {
5359            pane.close_active_item(
5360                &CloseActiveItem {
5361                    save_intent: None,
5362                    close_pinned: false,
5363                },
5364                window,
5365                cx,
5366            )
5367        })
5368        .await
5369        .unwrap();
5370        assert_item_labels(&pane, ["A", "C*"], cx);
5371
5372        pane.update_in(cx, |pane, window, cx| {
5373            pane.close_active_item(
5374                &CloseActiveItem {
5375                    save_intent: None,
5376                    close_pinned: false,
5377                },
5378                window,
5379                cx,
5380            )
5381        })
5382        .await
5383        .unwrap();
5384        assert_item_labels(&pane, ["A*"], cx);
5385    }
5386
5387    #[gpui::test]
5388    async fn test_remove_item_ordering_neighbour(cx: &mut TestAppContext) {
5389        init_test(cx);
5390        cx.update_global::<SettingsStore, ()>(|s, cx| {
5391            s.update_user_settings::<ItemSettings>(cx, |s| {
5392                s.activate_on_close = Some(ActivateOnClose::Neighbour);
5393            });
5394        });
5395        let fs = FakeFs::new(cx.executor());
5396
5397        let project = Project::test(fs, None, cx).await;
5398        let (workspace, cx) =
5399            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
5400        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
5401
5402        add_labeled_item(&pane, "A", false, cx);
5403        add_labeled_item(&pane, "B", false, cx);
5404        add_labeled_item(&pane, "C", false, cx);
5405        add_labeled_item(&pane, "D", false, cx);
5406        assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
5407
5408        pane.update_in(cx, |pane, window, cx| {
5409            pane.activate_item(1, false, false, window, cx)
5410        });
5411        add_labeled_item(&pane, "1", false, cx);
5412        assert_item_labels(&pane, ["A", "B", "1*", "C", "D"], cx);
5413
5414        pane.update_in(cx, |pane, window, cx| {
5415            pane.close_active_item(
5416                &CloseActiveItem {
5417                    save_intent: None,
5418                    close_pinned: false,
5419                },
5420                window,
5421                cx,
5422            )
5423        })
5424        .await
5425        .unwrap();
5426        assert_item_labels(&pane, ["A", "B", "C*", "D"], cx);
5427
5428        pane.update_in(cx, |pane, window, cx| {
5429            pane.activate_item(3, false, false, window, cx)
5430        });
5431        assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
5432
5433        pane.update_in(cx, |pane, window, cx| {
5434            pane.close_active_item(
5435                &CloseActiveItem {
5436                    save_intent: None,
5437                    close_pinned: false,
5438                },
5439                window,
5440                cx,
5441            )
5442        })
5443        .await
5444        .unwrap();
5445        assert_item_labels(&pane, ["A", "B", "C*"], cx);
5446
5447        pane.update_in(cx, |pane, window, cx| {
5448            pane.close_active_item(
5449                &CloseActiveItem {
5450                    save_intent: None,
5451                    close_pinned: false,
5452                },
5453                window,
5454                cx,
5455            )
5456        })
5457        .await
5458        .unwrap();
5459        assert_item_labels(&pane, ["A", "B*"], cx);
5460
5461        pane.update_in(cx, |pane, window, cx| {
5462            pane.close_active_item(
5463                &CloseActiveItem {
5464                    save_intent: None,
5465                    close_pinned: false,
5466                },
5467                window,
5468                cx,
5469            )
5470        })
5471        .await
5472        .unwrap();
5473        assert_item_labels(&pane, ["A*"], cx);
5474    }
5475
5476    #[gpui::test]
5477    async fn test_remove_item_ordering_left_neighbour(cx: &mut TestAppContext) {
5478        init_test(cx);
5479        cx.update_global::<SettingsStore, ()>(|s, cx| {
5480            s.update_user_settings::<ItemSettings>(cx, |s| {
5481                s.activate_on_close = Some(ActivateOnClose::LeftNeighbour);
5482            });
5483        });
5484        let fs = FakeFs::new(cx.executor());
5485
5486        let project = Project::test(fs, None, cx).await;
5487        let (workspace, cx) =
5488            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
5489        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
5490
5491        add_labeled_item(&pane, "A", false, cx);
5492        add_labeled_item(&pane, "B", false, cx);
5493        add_labeled_item(&pane, "C", false, cx);
5494        add_labeled_item(&pane, "D", false, cx);
5495        assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
5496
5497        pane.update_in(cx, |pane, window, cx| {
5498            pane.activate_item(1, false, false, window, cx)
5499        });
5500        add_labeled_item(&pane, "1", false, cx);
5501        assert_item_labels(&pane, ["A", "B", "1*", "C", "D"], cx);
5502
5503        pane.update_in(cx, |pane, window, cx| {
5504            pane.close_active_item(
5505                &CloseActiveItem {
5506                    save_intent: None,
5507                    close_pinned: false,
5508                },
5509                window,
5510                cx,
5511            )
5512        })
5513        .await
5514        .unwrap();
5515        assert_item_labels(&pane, ["A", "B*", "C", "D"], cx);
5516
5517        pane.update_in(cx, |pane, window, cx| {
5518            pane.activate_item(3, false, false, window, cx)
5519        });
5520        assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
5521
5522        pane.update_in(cx, |pane, window, cx| {
5523            pane.close_active_item(
5524                &CloseActiveItem {
5525                    save_intent: None,
5526                    close_pinned: false,
5527                },
5528                window,
5529                cx,
5530            )
5531        })
5532        .await
5533        .unwrap();
5534        assert_item_labels(&pane, ["A", "B", "C*"], cx);
5535
5536        pane.update_in(cx, |pane, window, cx| {
5537            pane.activate_item(0, false, false, window, cx)
5538        });
5539        assert_item_labels(&pane, ["A*", "B", "C"], cx);
5540
5541        pane.update_in(cx, |pane, window, cx| {
5542            pane.close_active_item(
5543                &CloseActiveItem {
5544                    save_intent: None,
5545                    close_pinned: false,
5546                },
5547                window,
5548                cx,
5549            )
5550        })
5551        .await
5552        .unwrap();
5553        assert_item_labels(&pane, ["B*", "C"], cx);
5554
5555        pane.update_in(cx, |pane, window, cx| {
5556            pane.close_active_item(
5557                &CloseActiveItem {
5558                    save_intent: None,
5559                    close_pinned: false,
5560                },
5561                window,
5562                cx,
5563            )
5564        })
5565        .await
5566        .unwrap();
5567        assert_item_labels(&pane, ["C*"], cx);
5568    }
5569
5570    #[gpui::test]
5571    async fn test_close_inactive_items(cx: &mut TestAppContext) {
5572        init_test(cx);
5573        let fs = FakeFs::new(cx.executor());
5574
5575        let project = Project::test(fs, None, cx).await;
5576        let (workspace, cx) =
5577            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
5578        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
5579
5580        let item_a = add_labeled_item(&pane, "A", false, cx);
5581        pane.update_in(cx, |pane, window, cx| {
5582            let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
5583            pane.pin_tab_at(ix, window, cx);
5584        });
5585        assert_item_labels(&pane, ["A*!"], cx);
5586
5587        let item_b = add_labeled_item(&pane, "B", false, cx);
5588        pane.update_in(cx, |pane, window, cx| {
5589            let ix = pane.index_for_item_id(item_b.item_id()).unwrap();
5590            pane.pin_tab_at(ix, window, cx);
5591        });
5592        assert_item_labels(&pane, ["A!", "B*!"], cx);
5593
5594        add_labeled_item(&pane, "C", false, cx);
5595        assert_item_labels(&pane, ["A!", "B!", "C*"], cx);
5596
5597        add_labeled_item(&pane, "D", false, cx);
5598        add_labeled_item(&pane, "E", false, cx);
5599        assert_item_labels(&pane, ["A!", "B!", "C", "D", "E*"], cx);
5600
5601        pane.update_in(cx, |pane, window, cx| {
5602            pane.close_inactive_items(
5603                &CloseInactiveItems {
5604                    save_intent: None,
5605                    close_pinned: false,
5606                },
5607                window,
5608                cx,
5609            )
5610        })
5611        .await
5612        .unwrap();
5613        assert_item_labels(&pane, ["A!", "B!", "E*"], cx);
5614    }
5615
5616    #[gpui::test]
5617    async fn test_close_clean_items(cx: &mut TestAppContext) {
5618        init_test(cx);
5619        let fs = FakeFs::new(cx.executor());
5620
5621        let project = Project::test(fs, None, cx).await;
5622        let (workspace, cx) =
5623            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
5624        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
5625
5626        add_labeled_item(&pane, "A", true, cx);
5627        add_labeled_item(&pane, "B", false, cx);
5628        add_labeled_item(&pane, "C", true, cx);
5629        add_labeled_item(&pane, "D", false, cx);
5630        add_labeled_item(&pane, "E", false, cx);
5631        assert_item_labels(&pane, ["A^", "B", "C^", "D", "E*"], cx);
5632
5633        pane.update_in(cx, |pane, window, cx| {
5634            pane.close_clean_items(
5635                &CloseCleanItems {
5636                    close_pinned: false,
5637                },
5638                window,
5639                cx,
5640            )
5641        })
5642        .await
5643        .unwrap();
5644        assert_item_labels(&pane, ["A^", "C*^"], cx);
5645    }
5646
5647    #[gpui::test]
5648    async fn test_close_items_to_the_left(cx: &mut TestAppContext) {
5649        init_test(cx);
5650        let fs = FakeFs::new(cx.executor());
5651
5652        let project = Project::test(fs, None, cx).await;
5653        let (workspace, cx) =
5654            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
5655        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
5656
5657        set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx);
5658
5659        pane.update_in(cx, |pane, window, cx| {
5660            pane.close_items_to_the_left_by_id(
5661                None,
5662                &CloseItemsToTheLeft {
5663                    close_pinned: false,
5664                },
5665                window,
5666                cx,
5667            )
5668        })
5669        .await
5670        .unwrap();
5671        assert_item_labels(&pane, ["C*", "D", "E"], cx);
5672    }
5673
5674    #[gpui::test]
5675    async fn test_close_items_to_the_right(cx: &mut TestAppContext) {
5676        init_test(cx);
5677        let fs = FakeFs::new(cx.executor());
5678
5679        let project = Project::test(fs, None, cx).await;
5680        let (workspace, cx) =
5681            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
5682        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
5683
5684        set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx);
5685
5686        pane.update_in(cx, |pane, window, cx| {
5687            pane.close_items_to_the_right_by_id(
5688                None,
5689                &CloseItemsToTheRight {
5690                    close_pinned: false,
5691                },
5692                window,
5693                cx,
5694            )
5695        })
5696        .await
5697        .unwrap();
5698        assert_item_labels(&pane, ["A", "B", "C*"], cx);
5699    }
5700
5701    #[gpui::test]
5702    async fn test_close_all_items(cx: &mut TestAppContext) {
5703        init_test(cx);
5704        let fs = FakeFs::new(cx.executor());
5705
5706        let project = Project::test(fs, None, cx).await;
5707        let (workspace, cx) =
5708            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
5709        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
5710
5711        let item_a = add_labeled_item(&pane, "A", false, cx);
5712        add_labeled_item(&pane, "B", false, cx);
5713        add_labeled_item(&pane, "C", false, cx);
5714        assert_item_labels(&pane, ["A", "B", "C*"], cx);
5715
5716        pane.update_in(cx, |pane, window, cx| {
5717            let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
5718            pane.pin_tab_at(ix, window, cx);
5719            pane.close_all_items(
5720                &CloseAllItems {
5721                    save_intent: None,
5722                    close_pinned: false,
5723                },
5724                window,
5725                cx,
5726            )
5727        })
5728        .await
5729        .unwrap();
5730        assert_item_labels(&pane, ["A*!"], cx);
5731
5732        pane.update_in(cx, |pane, window, cx| {
5733            let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
5734            pane.unpin_tab_at(ix, window, cx);
5735            pane.close_all_items(
5736                &CloseAllItems {
5737                    save_intent: None,
5738                    close_pinned: false,
5739                },
5740                window,
5741                cx,
5742            )
5743        })
5744        .await
5745        .unwrap();
5746
5747        assert_item_labels(&pane, [], cx);
5748
5749        add_labeled_item(&pane, "A", true, cx).update(cx, |item, cx| {
5750            item.project_items
5751                .push(TestProjectItem::new_dirty(1, "A.txt", cx))
5752        });
5753        add_labeled_item(&pane, "B", true, cx).update(cx, |item, cx| {
5754            item.project_items
5755                .push(TestProjectItem::new_dirty(2, "B.txt", cx))
5756        });
5757        add_labeled_item(&pane, "C", true, cx).update(cx, |item, cx| {
5758            item.project_items
5759                .push(TestProjectItem::new_dirty(3, "C.txt", cx))
5760        });
5761        assert_item_labels(&pane, ["A^", "B^", "C*^"], cx);
5762
5763        let save = pane.update_in(cx, |pane, window, cx| {
5764            pane.close_all_items(
5765                &CloseAllItems {
5766                    save_intent: None,
5767                    close_pinned: false,
5768                },
5769                window,
5770                cx,
5771            )
5772        });
5773
5774        cx.executor().run_until_parked();
5775        cx.simulate_prompt_answer("Save all");
5776        save.await.unwrap();
5777        assert_item_labels(&pane, [], cx);
5778
5779        add_labeled_item(&pane, "A", true, cx);
5780        add_labeled_item(&pane, "B", true, cx);
5781        add_labeled_item(&pane, "C", true, cx);
5782        assert_item_labels(&pane, ["A^", "B^", "C*^"], cx);
5783        let save = pane.update_in(cx, |pane, window, cx| {
5784            pane.close_all_items(
5785                &CloseAllItems {
5786                    save_intent: None,
5787                    close_pinned: false,
5788                },
5789                window,
5790                cx,
5791            )
5792        });
5793
5794        cx.executor().run_until_parked();
5795        cx.simulate_prompt_answer("Discard all");
5796        save.await.unwrap();
5797        assert_item_labels(&pane, [], cx);
5798    }
5799
5800    #[gpui::test]
5801    async fn test_close_with_save_intent(cx: &mut TestAppContext) {
5802        init_test(cx);
5803        let fs = FakeFs::new(cx.executor());
5804
5805        let project = Project::test(fs, None, cx).await;
5806        let (workspace, cx) =
5807            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
5808        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
5809
5810        let a = cx.update(|_, cx| TestProjectItem::new_dirty(1, "A.txt", cx));
5811        let b = cx.update(|_, cx| TestProjectItem::new_dirty(1, "B.txt", cx));
5812        let c = cx.update(|_, cx| TestProjectItem::new_dirty(1, "C.txt", cx));
5813
5814        add_labeled_item(&pane, "AB", true, cx).update(cx, |item, _| {
5815            item.project_items.push(a.clone());
5816            item.project_items.push(b.clone());
5817        });
5818        add_labeled_item(&pane, "C", true, cx)
5819            .update(cx, |item, _| item.project_items.push(c.clone()));
5820        assert_item_labels(&pane, ["AB^", "C*^"], cx);
5821
5822        pane.update_in(cx, |pane, window, cx| {
5823            pane.close_all_items(
5824                &CloseAllItems {
5825                    save_intent: Some(SaveIntent::Save),
5826                    close_pinned: false,
5827                },
5828                window,
5829                cx,
5830            )
5831        })
5832        .await
5833        .unwrap();
5834
5835        assert_item_labels(&pane, [], cx);
5836        cx.update(|_, cx| {
5837            assert!(!a.read(cx).is_dirty);
5838            assert!(!b.read(cx).is_dirty);
5839            assert!(!c.read(cx).is_dirty);
5840        });
5841    }
5842
5843    #[gpui::test]
5844    async fn test_close_all_items_including_pinned(cx: &mut TestAppContext) {
5845        init_test(cx);
5846        let fs = FakeFs::new(cx.executor());
5847
5848        let project = Project::test(fs, None, cx).await;
5849        let (workspace, cx) =
5850            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
5851        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
5852
5853        let item_a = add_labeled_item(&pane, "A", false, cx);
5854        add_labeled_item(&pane, "B", false, cx);
5855        add_labeled_item(&pane, "C", false, cx);
5856        assert_item_labels(&pane, ["A", "B", "C*"], cx);
5857
5858        pane.update_in(cx, |pane, window, cx| {
5859            let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
5860            pane.pin_tab_at(ix, window, cx);
5861            pane.close_all_items(
5862                &CloseAllItems {
5863                    save_intent: None,
5864                    close_pinned: true,
5865                },
5866                window,
5867                cx,
5868            )
5869        })
5870        .await
5871        .unwrap();
5872        assert_item_labels(&pane, [], cx);
5873    }
5874
5875    #[gpui::test]
5876    async fn test_close_pinned_tab_with_non_pinned_in_same_pane(cx: &mut TestAppContext) {
5877        init_test(cx);
5878        let fs = FakeFs::new(cx.executor());
5879        let project = Project::test(fs, None, cx).await;
5880        let (workspace, cx) =
5881            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
5882
5883        // Non-pinned tabs in same pane
5884        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
5885        add_labeled_item(&pane, "A", false, cx);
5886        add_labeled_item(&pane, "B", false, cx);
5887        add_labeled_item(&pane, "C", false, cx);
5888        pane.update_in(cx, |pane, window, cx| {
5889            pane.pin_tab_at(0, window, cx);
5890        });
5891        set_labeled_items(&pane, ["A*", "B", "C"], cx);
5892        pane.update_in(cx, |pane, window, cx| {
5893            pane.close_active_item(
5894                &CloseActiveItem {
5895                    save_intent: None,
5896                    close_pinned: false,
5897                },
5898                window,
5899                cx,
5900            )
5901            .unwrap();
5902        });
5903        // Non-pinned tab should be active
5904        assert_item_labels(&pane, ["A!", "B*", "C"], cx);
5905    }
5906
5907    #[gpui::test]
5908    async fn test_close_pinned_tab_with_non_pinned_in_different_pane(cx: &mut TestAppContext) {
5909        init_test(cx);
5910        let fs = FakeFs::new(cx.executor());
5911        let project = Project::test(fs, None, cx).await;
5912        let (workspace, cx) =
5913            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
5914
5915        // No non-pinned tabs in same pane, non-pinned tabs in another pane
5916        let pane1 = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
5917        let pane2 = workspace.update_in(cx, |workspace, window, cx| {
5918            workspace.split_pane(pane1.clone(), SplitDirection::Right, window, cx)
5919        });
5920        add_labeled_item(&pane1, "A", false, cx);
5921        pane1.update_in(cx, |pane, window, cx| {
5922            pane.pin_tab_at(0, window, cx);
5923        });
5924        set_labeled_items(&pane1, ["A*"], cx);
5925        add_labeled_item(&pane2, "B", false, cx);
5926        set_labeled_items(&pane2, ["B"], cx);
5927        pane1.update_in(cx, |pane, window, cx| {
5928            pane.close_active_item(
5929                &CloseActiveItem {
5930                    save_intent: None,
5931                    close_pinned: false,
5932                },
5933                window,
5934                cx,
5935            )
5936            .unwrap();
5937        });
5938        //  Non-pinned tab of other pane should be active
5939        assert_item_labels(&pane2, ["B*"], cx);
5940    }
5941
5942    #[gpui::test]
5943    async fn ensure_item_closing_actions_do_not_panic_when_no_items_exist(cx: &mut TestAppContext) {
5944        init_test(cx);
5945        let fs = FakeFs::new(cx.executor());
5946        let project = Project::test(fs, None, cx).await;
5947        let (workspace, cx) =
5948            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
5949
5950        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
5951        assert_item_labels(&pane, [], cx);
5952
5953        pane.update_in(cx, |pane, window, cx| {
5954            pane.close_active_item(
5955                &CloseActiveItem {
5956                    save_intent: None,
5957                    close_pinned: false,
5958                },
5959                window,
5960                cx,
5961            )
5962        })
5963        .await
5964        .unwrap();
5965
5966        pane.update_in(cx, |pane, window, cx| {
5967            pane.close_inactive_items(
5968                &CloseInactiveItems {
5969                    save_intent: None,
5970                    close_pinned: false,
5971                },
5972                window,
5973                cx,
5974            )
5975        })
5976        .await
5977        .unwrap();
5978
5979        pane.update_in(cx, |pane, window, cx| {
5980            pane.close_all_items(
5981                &CloseAllItems {
5982                    save_intent: None,
5983                    close_pinned: false,
5984                },
5985                window,
5986                cx,
5987            )
5988        })
5989        .await
5990        .unwrap();
5991
5992        pane.update_in(cx, |pane, window, cx| {
5993            pane.close_clean_items(
5994                &CloseCleanItems {
5995                    close_pinned: false,
5996                },
5997                window,
5998                cx,
5999            )
6000        })
6001        .await
6002        .unwrap();
6003
6004        pane.update_in(cx, |pane, window, cx| {
6005            pane.close_items_to_the_right_by_id(
6006                None,
6007                &CloseItemsToTheRight {
6008                    close_pinned: false,
6009                },
6010                window,
6011                cx,
6012            )
6013        })
6014        .await
6015        .unwrap();
6016
6017        pane.update_in(cx, |pane, window, cx| {
6018            pane.close_items_to_the_left_by_id(
6019                None,
6020                &CloseItemsToTheLeft {
6021                    close_pinned: false,
6022                },
6023                window,
6024                cx,
6025            )
6026        })
6027        .await
6028        .unwrap();
6029    }
6030
6031    fn init_test(cx: &mut TestAppContext) {
6032        cx.update(|cx| {
6033            let settings_store = SettingsStore::test(cx);
6034            cx.set_global(settings_store);
6035            theme::init(LoadThemes::JustBase, cx);
6036            crate::init_settings(cx);
6037            Project::init_settings(cx);
6038        });
6039    }
6040
6041    fn set_max_tabs(cx: &mut TestAppContext, value: Option<usize>) {
6042        cx.update_global(|store: &mut SettingsStore, cx| {
6043            store.update_user_settings::<WorkspaceSettings>(cx, |settings| {
6044                settings.max_tabs = value.map(|v| NonZero::new(v).unwrap())
6045            });
6046        });
6047    }
6048
6049    fn add_labeled_item(
6050        pane: &Entity<Pane>,
6051        label: &str,
6052        is_dirty: bool,
6053        cx: &mut VisualTestContext,
6054    ) -> Box<Entity<TestItem>> {
6055        pane.update_in(cx, |pane, window, cx| {
6056            let labeled_item =
6057                Box::new(cx.new(|cx| TestItem::new(cx).with_label(label).with_dirty(is_dirty)));
6058            pane.add_item(labeled_item.clone(), false, false, None, window, cx);
6059            labeled_item
6060        })
6061    }
6062
6063    fn set_labeled_items<const COUNT: usize>(
6064        pane: &Entity<Pane>,
6065        labels: [&str; COUNT],
6066        cx: &mut VisualTestContext,
6067    ) -> [Box<Entity<TestItem>>; COUNT] {
6068        pane.update_in(cx, |pane, window, cx| {
6069            pane.items.clear();
6070            let mut active_item_index = 0;
6071
6072            let mut index = 0;
6073            let items = labels.map(|mut label| {
6074                if label.ends_with('*') {
6075                    label = label.trim_end_matches('*');
6076                    active_item_index = index;
6077                }
6078
6079                let labeled_item = Box::new(cx.new(|cx| TestItem::new(cx).with_label(label)));
6080                pane.add_item(labeled_item.clone(), false, false, None, window, cx);
6081                index += 1;
6082                labeled_item
6083            });
6084
6085            pane.activate_item(active_item_index, false, false, window, cx);
6086
6087            items
6088        })
6089    }
6090
6091    // Assert the item label, with the active item label suffixed with a '*'
6092    #[track_caller]
6093    fn assert_item_labels<const COUNT: usize>(
6094        pane: &Entity<Pane>,
6095        expected_states: [&str; COUNT],
6096        cx: &mut VisualTestContext,
6097    ) {
6098        let actual_states = pane.update(cx, |pane, cx| {
6099            pane.items
6100                .iter()
6101                .enumerate()
6102                .map(|(ix, item)| {
6103                    let mut state = item
6104                        .to_any()
6105                        .downcast::<TestItem>()
6106                        .unwrap()
6107                        .read(cx)
6108                        .label
6109                        .clone();
6110                    if ix == pane.active_item_index {
6111                        state.push('*');
6112                    }
6113                    if item.is_dirty(cx) {
6114                        state.push('^');
6115                    }
6116                    if pane.is_tab_pinned(ix) {
6117                        state.push('!');
6118                    }
6119                    state
6120                })
6121                .collect::<Vec<_>>()
6122        });
6123        assert_eq!(
6124            actual_states, expected_states,
6125            "pane items do not match expectation"
6126        );
6127    }
6128}