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