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