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