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