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