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