pane.rs

   1use crate::{
   2    CloseWindow, NewFile, NewTerminal, OpenInTerminal, OpenOptions, OpenTerminal, OpenVisible,
   3    SplitDirection, ToggleFileFinder, ToggleProjectSymbols, ToggleZoom, Workspace,
   4    WorkspaceItemBuilder,
   5    item::{
   6        ActivateOnClose, ClosePosition, Item, ItemHandle, ItemSettings, PreviewTabsSettings,
   7        ProjectItemKind, ShowCloseButton, ShowDiagnostics, TabContentParams, TabTooltipContent,
   8        WeakItemHandle,
   9    },
  10    move_item,
  11    notifications::NotifyResultExt,
  12    toolbar::Toolbar,
  13    workspace_settings::{AutosaveSetting, TabBarSettings, WorkspaceSettings},
  14};
  15use anyhow::Result;
  16use collections::{BTreeSet, HashMap, HashSet, VecDeque};
  17use futures::{StreamExt, stream::FuturesUnordered};
  18use gpui::{
  19    Action, AnyElement, App, AsyncWindowContext, ClickEvent, ClipboardItem, Context, Corner, Div,
  20    DragMoveEvent, Entity, EntityId, EventEmitter, ExternalPaths, FocusHandle, FocusOutEvent,
  21    Focusable, KeyContext, MouseButton, MouseDownEvent, NavigationDirection, Pixels, Point,
  22    PromptLevel, Render, ScrollHandle, Subscription, Task, WeakEntity, WeakFocusHandle, Window,
  23    actions, anchored, deferred, impl_actions, prelude::*,
  24};
  25use itertools::Itertools;
  26use language::DiagnosticSeverity;
  27use parking_lot::Mutex;
  28use project::{DirectoryLister, Project, ProjectEntryId, ProjectPath, WorktreeId};
  29use schemars::JsonSchema;
  30use serde::Deserialize;
  31use settings::{Settings, SettingsStore};
  32use std::{
  33    any::Any,
  34    cmp, fmt, mem,
  35    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(should_format, project, window, cx)
1874                        })?
1875                        .await?
1876                    }
1877                    Ok(1) => {
1878                        pane.update_in(cx, |pane, window, cx| {
1879                            pane.remove_item(item.item_id(), false, true, window, cx)
1880                        })?;
1881                    }
1882                    _ => return Ok(false),
1883                }
1884                return Ok(true);
1885            } else {
1886                let answer = pane.update_in(cx, |pane, window, cx| {
1887                    pane.activate_item(item_ix, true, true, window, cx);
1888                    window.prompt(
1889                        PromptLevel::Warning,
1890                        CONFLICT_MESSAGE,
1891                        None,
1892                        &["Overwrite", "Discard", "Cancel"],
1893                        cx,
1894                    )
1895                })?;
1896                match answer.await {
1897                    Ok(0) => {
1898                        pane.update_in(cx, |_, window, cx| {
1899                            item.save(should_format, project, window, cx)
1900                        })?
1901                        .await?
1902                    }
1903                    Ok(1) => {
1904                        pane.update_in(cx, |_, window, cx| item.reload(project, window, cx))?
1905                            .await?
1906                    }
1907                    _ => return Ok(false),
1908                }
1909            }
1910        } else if is_dirty && (can_save || can_save_as) {
1911            if save_intent == SaveIntent::Close {
1912                let will_autosave = cx.update(|_window, cx| {
1913                    matches!(
1914                        item.workspace_settings(cx).autosave,
1915                        AutosaveSetting::OnFocusChange | AutosaveSetting::OnWindowChange
1916                    ) && item.can_autosave(cx)
1917                })?;
1918                if !will_autosave {
1919                    let item_id = item.item_id();
1920                    let answer_task = pane.update_in(cx, |pane, window, cx| {
1921                        if pane.save_modals_spawned.insert(item_id) {
1922                            pane.activate_item(item_ix, true, true, window, cx);
1923                            let prompt = dirty_message_for(item.project_path(cx));
1924                            Some(window.prompt(
1925                                PromptLevel::Warning,
1926                                &prompt,
1927                                None,
1928                                &["Save", "Don't Save", "Cancel"],
1929                                cx,
1930                            ))
1931                        } else {
1932                            None
1933                        }
1934                    })?;
1935                    if let Some(answer_task) = answer_task {
1936                        let answer = answer_task.await;
1937                        pane.update(cx, |pane, _| {
1938                            if !pane.save_modals_spawned.remove(&item_id) {
1939                                debug_panic!(
1940                                    "save modal was not present in spawned modals after awaiting for its answer"
1941                                )
1942                            }
1943                        })?;
1944                        match answer {
1945                            Ok(0) => {}
1946                            Ok(1) => {
1947                                // Don't save this file
1948                                pane.update_in(cx, |pane, window, cx| {
1949                                    if pane.is_tab_pinned(item_ix) && !item.can_save(cx) {
1950                                        pane.pinned_tab_count -= 1;
1951                                    }
1952                                    item.discarded(project, window, cx)
1953                                })
1954                                .log_err();
1955                                return Ok(true);
1956                            }
1957                            _ => return Ok(false), // Cancel
1958                        }
1959                    } else {
1960                        return Ok(false);
1961                    }
1962                }
1963            }
1964
1965            if can_save {
1966                pane.update_in(cx, |pane, window, cx| {
1967                    if pane.is_active_preview_item(item.item_id()) {
1968                        pane.set_preview_item_id(None, cx);
1969                    }
1970                    item.save(should_format, project, window, cx)
1971                })?
1972                .await?;
1973            } else if can_save_as && is_singleton {
1974                let new_path = pane.update_in(cx, |pane, window, cx| {
1975                    pane.activate_item(item_ix, true, true, window, cx);
1976                    pane.workspace.update(cx, |workspace, cx| {
1977                        let lister = if workspace.project().read(cx).is_local() {
1978                            DirectoryLister::Local(
1979                                workspace.project().clone(),
1980                                workspace.app_state().fs.clone(),
1981                            )
1982                        } else {
1983                            DirectoryLister::Project(workspace.project().clone())
1984                        };
1985                        workspace.prompt_for_new_path(lister, window, cx)
1986                    })
1987                })??;
1988                let Some(new_path) = new_path.await.ok().flatten().into_iter().flatten().next()
1989                else {
1990                    return Ok(false);
1991                };
1992
1993                let project_path = pane
1994                    .update(cx, |pane, cx| {
1995                        pane.project
1996                            .update(cx, |project, cx| {
1997                                project.find_or_create_worktree(new_path, true, cx)
1998                            })
1999                            .ok()
2000                    })
2001                    .ok()
2002                    .flatten();
2003                let save_task = if let Some(project_path) = project_path {
2004                    let (worktree, path) = project_path.await?;
2005                    let worktree_id = worktree.read_with(cx, |worktree, _| worktree.id())?;
2006                    let new_path = ProjectPath {
2007                        worktree_id,
2008                        path: path.into(),
2009                    };
2010
2011                    pane.update_in(cx, |pane, window, cx| {
2012                        if let Some(item) = pane.item_for_path(new_path.clone(), cx) {
2013                            pane.remove_item(item.item_id(), false, false, window, cx);
2014                        }
2015
2016                        item.save_as(project, new_path, window, cx)
2017                    })?
2018                } else {
2019                    return Ok(false);
2020                };
2021
2022                save_task.await?;
2023                return Ok(true);
2024            }
2025        }
2026
2027        pane.update(cx, |_, cx| {
2028            cx.emit(Event::UserSavedItem {
2029                item: item.downgrade_item(),
2030                save_intent,
2031            });
2032            true
2033        })
2034    }
2035
2036    pub fn autosave_item(
2037        item: &dyn ItemHandle,
2038        project: Entity<Project>,
2039        window: &mut Window,
2040        cx: &mut App,
2041    ) -> Task<Result<()>> {
2042        let format = !matches!(
2043            item.workspace_settings(cx).autosave,
2044            AutosaveSetting::AfterDelay { .. }
2045        );
2046        if item.can_autosave(cx) {
2047            item.save(format, project, window, cx)
2048        } else {
2049            Task::ready(Ok(()))
2050        }
2051    }
2052
2053    pub fn focus_active_item(&mut self, window: &mut Window, cx: &mut Context<Self>) {
2054        if let Some(active_item) = self.active_item() {
2055            let focus_handle = active_item.item_focus_handle(cx);
2056            window.focus(&focus_handle);
2057        }
2058    }
2059
2060    pub fn split(&mut self, direction: SplitDirection, cx: &mut Context<Self>) {
2061        cx.emit(Event::Split(direction));
2062    }
2063
2064    pub fn toolbar(&self) -> &Entity<Toolbar> {
2065        &self.toolbar
2066    }
2067
2068    pub fn handle_deleted_project_item(
2069        &mut self,
2070        entry_id: ProjectEntryId,
2071        window: &mut Window,
2072        cx: &mut Context<Pane>,
2073    ) -> Option<()> {
2074        let item_id = self.items().find_map(|item| {
2075            if item.is_singleton(cx) && item.project_entry_ids(cx).as_slice() == [entry_id] {
2076                Some(item.item_id())
2077            } else {
2078                None
2079            }
2080        })?;
2081
2082        self.remove_item(item_id, false, true, window, cx);
2083        self.nav_history.remove_item(item_id);
2084
2085        Some(())
2086    }
2087
2088    fn update_toolbar(&mut self, window: &mut Window, cx: &mut Context<Self>) {
2089        let active_item = self
2090            .items
2091            .get(self.active_item_index)
2092            .map(|item| item.as_ref());
2093        self.toolbar.update(cx, |toolbar, cx| {
2094            toolbar.set_active_item(active_item, window, cx);
2095        });
2096    }
2097
2098    fn update_status_bar(&mut self, window: &mut Window, cx: &mut Context<Self>) {
2099        let workspace = self.workspace.clone();
2100        let pane = cx.entity().clone();
2101
2102        window.defer(cx, move |window, cx| {
2103            let Ok(status_bar) =
2104                workspace.read_with(cx, |workspace, _| workspace.status_bar.clone())
2105            else {
2106                return;
2107            };
2108
2109            status_bar.update(cx, move |status_bar, cx| {
2110                status_bar.set_active_pane(&pane, window, cx);
2111            });
2112        });
2113    }
2114
2115    fn entry_abs_path(&self, entry: ProjectEntryId, cx: &App) -> Option<PathBuf> {
2116        let worktree = self
2117            .workspace
2118            .upgrade()?
2119            .read(cx)
2120            .project()
2121            .read(cx)
2122            .worktree_for_entry(entry, cx)?
2123            .read(cx);
2124        let entry = worktree.entry_for_id(entry)?;
2125        match &entry.canonical_path {
2126            Some(canonical_path) => Some(canonical_path.to_path_buf()),
2127            None => worktree.absolutize(&entry.path).ok(),
2128        }
2129    }
2130
2131    pub fn icon_color(selected: bool) -> Color {
2132        if selected {
2133            Color::Default
2134        } else {
2135            Color::Muted
2136        }
2137    }
2138
2139    fn toggle_pin_tab(&mut self, _: &TogglePinTab, window: &mut Window, cx: &mut Context<Self>) {
2140        if self.items.is_empty() {
2141            return;
2142        }
2143        let active_tab_ix = self.active_item_index();
2144        if self.is_tab_pinned(active_tab_ix) {
2145            self.unpin_tab_at(active_tab_ix, window, cx);
2146        } else {
2147            self.pin_tab_at(active_tab_ix, window, cx);
2148        }
2149    }
2150
2151    fn unpin_all_tabs(&mut self, _: &UnpinAllTabs, window: &mut Window, cx: &mut Context<Self>) {
2152        if self.items.is_empty() {
2153            return;
2154        }
2155
2156        let pinned_item_ids = self.pinned_item_ids().into_iter().rev();
2157
2158        for pinned_item_id in pinned_item_ids {
2159            if let Some(ix) = self.index_for_item_id(pinned_item_id) {
2160                self.unpin_tab_at(ix, window, cx);
2161            }
2162        }
2163    }
2164
2165    fn pin_tab_at(&mut self, ix: usize, window: &mut Window, cx: &mut Context<Self>) {
2166        self.change_tab_pin_state(ix, PinOperation::Pin, window, cx);
2167    }
2168
2169    fn unpin_tab_at(&mut self, ix: usize, window: &mut Window, cx: &mut Context<Self>) {
2170        self.change_tab_pin_state(ix, PinOperation::Unpin, window, cx);
2171    }
2172
2173    fn change_tab_pin_state(
2174        &mut self,
2175        ix: usize,
2176        operation: PinOperation,
2177        window: &mut Window,
2178        cx: &mut Context<Self>,
2179    ) {
2180        maybe!({
2181            let pane = cx.entity().clone();
2182
2183            let destination_index = match operation {
2184                PinOperation::Pin => self.pinned_tab_count.min(ix),
2185                PinOperation::Unpin => self.pinned_tab_count.checked_sub(1)?,
2186            };
2187
2188            let id = self.item_for_index(ix)?.item_id();
2189            let should_activate = ix == self.active_item_index;
2190
2191            if matches!(operation, PinOperation::Pin) && self.is_active_preview_item(id) {
2192                self.set_preview_item_id(None, cx);
2193            }
2194
2195            match operation {
2196                PinOperation::Pin => self.pinned_tab_count += 1,
2197                PinOperation::Unpin => self.pinned_tab_count -= 1,
2198            }
2199
2200            if ix == destination_index {
2201                cx.notify();
2202            } else {
2203                self.workspace
2204                    .update(cx, |_, cx| {
2205                        cx.defer_in(window, move |_, window, cx| {
2206                            move_item(
2207                                &pane,
2208                                &pane,
2209                                id,
2210                                destination_index,
2211                                should_activate,
2212                                window,
2213                                cx,
2214                            );
2215                        });
2216                    })
2217                    .ok()?;
2218            }
2219
2220            let event = match operation {
2221                PinOperation::Pin => Event::ItemPinned,
2222                PinOperation::Unpin => Event::ItemUnpinned,
2223            };
2224
2225            cx.emit(event);
2226
2227            Some(())
2228        });
2229    }
2230
2231    fn is_tab_pinned(&self, ix: usize) -> bool {
2232        self.pinned_tab_count > ix
2233    }
2234
2235    fn has_unpinned_tabs(&self) -> bool {
2236        self.pinned_tab_count < self.items.len()
2237    }
2238
2239    fn activate_unpinned_tab(&mut self, window: &mut Window, cx: &mut Context<Self>) {
2240        if self.items.is_empty() {
2241            return;
2242        }
2243        let Some(index) = self
2244            .items()
2245            .enumerate()
2246            .find_map(|(index, _item)| (!self.is_tab_pinned(index)).then_some(index))
2247        else {
2248            return;
2249        };
2250        self.activate_item(index, true, true, window, cx);
2251    }
2252
2253    fn render_tab(
2254        &self,
2255        ix: usize,
2256        item: &dyn ItemHandle,
2257        detail: usize,
2258        focus_handle: &FocusHandle,
2259        window: &mut Window,
2260        cx: &mut Context<Pane>,
2261    ) -> impl IntoElement + use<> {
2262        let is_active = ix == self.active_item_index;
2263        let is_preview = self
2264            .preview_item_id
2265            .map(|id| id == item.item_id())
2266            .unwrap_or(false);
2267
2268        let label = item.tab_content(
2269            TabContentParams {
2270                detail: Some(detail),
2271                selected: is_active,
2272                preview: is_preview,
2273                deemphasized: !self.has_focus(window, cx),
2274            },
2275            window,
2276            cx,
2277        );
2278
2279        let item_diagnostic = item
2280            .project_path(cx)
2281            .map_or(None, |project_path| self.diagnostics.get(&project_path));
2282
2283        let decorated_icon = item_diagnostic.map_or(None, |diagnostic| {
2284            let icon = match item.tab_icon(window, cx) {
2285                Some(icon) => icon,
2286                None => return None,
2287            };
2288
2289            let knockout_item_color = if is_active {
2290                cx.theme().colors().tab_active_background
2291            } else {
2292                cx.theme().colors().tab_bar_background
2293            };
2294
2295            let (icon_decoration, icon_color) = if matches!(diagnostic, &DiagnosticSeverity::ERROR)
2296            {
2297                (IconDecorationKind::X, Color::Error)
2298            } else {
2299                (IconDecorationKind::Triangle, Color::Warning)
2300            };
2301
2302            Some(DecoratedIcon::new(
2303                icon.size(IconSize::Small).color(Color::Muted),
2304                Some(
2305                    IconDecoration::new(icon_decoration, knockout_item_color, cx)
2306                        .color(icon_color.color(cx))
2307                        .position(Point {
2308                            x: px(-2.),
2309                            y: px(-2.),
2310                        }),
2311                ),
2312            ))
2313        });
2314
2315        let icon = if decorated_icon.is_none() {
2316            match item_diagnostic {
2317                Some(&DiagnosticSeverity::ERROR) => None,
2318                Some(&DiagnosticSeverity::WARNING) => None,
2319                _ => item
2320                    .tab_icon(window, cx)
2321                    .map(|icon| icon.color(Color::Muted)),
2322            }
2323            .map(|icon| icon.size(IconSize::Small))
2324        } else {
2325            None
2326        };
2327
2328        let settings = ItemSettings::get_global(cx);
2329        let close_side = &settings.close_position;
2330        let show_close_button = &settings.show_close_button;
2331        let indicator = render_item_indicator(item.boxed_clone(), cx);
2332        let item_id = item.item_id();
2333        let is_first_item = ix == 0;
2334        let is_last_item = ix == self.items.len() - 1;
2335        let is_pinned = self.is_tab_pinned(ix);
2336        let position_relative_to_active_item = ix.cmp(&self.active_item_index);
2337
2338        let tab = Tab::new(ix)
2339            .position(if is_first_item {
2340                TabPosition::First
2341            } else if is_last_item {
2342                TabPosition::Last
2343            } else {
2344                TabPosition::Middle(position_relative_to_active_item)
2345            })
2346            .close_side(match close_side {
2347                ClosePosition::Left => ui::TabCloseSide::Start,
2348                ClosePosition::Right => ui::TabCloseSide::End,
2349            })
2350            .toggle_state(is_active)
2351            .on_click(cx.listener(move |pane: &mut Self, _, window, cx| {
2352                pane.activate_item(ix, true, true, window, cx)
2353            }))
2354            // TODO: This should be a click listener with the middle mouse button instead of a mouse down listener.
2355            .on_mouse_down(
2356                MouseButton::Middle,
2357                cx.listener(move |pane, _event, window, cx| {
2358                    pane.close_item_by_id(item_id, SaveIntent::Close, window, cx)
2359                        .detach_and_log_err(cx);
2360                }),
2361            )
2362            .on_mouse_down(
2363                MouseButton::Left,
2364                cx.listener(move |pane, event: &MouseDownEvent, _, cx| {
2365                    if let Some(id) = pane.preview_item_id {
2366                        if id == item_id && event.click_count > 1 {
2367                            pane.set_preview_item_id(None, cx);
2368                        }
2369                    }
2370                }),
2371            )
2372            .on_drag(
2373                DraggedTab {
2374                    item: item.boxed_clone(),
2375                    pane: cx.entity().clone(),
2376                    detail,
2377                    is_active,
2378                    ix,
2379                },
2380                |tab, _, _, cx| cx.new(|_| tab.clone()),
2381            )
2382            .drag_over::<DraggedTab>(|tab, _, _, cx| {
2383                tab.bg(cx.theme().colors().drop_target_background)
2384            })
2385            .drag_over::<DraggedSelection>(|tab, _, _, cx| {
2386                tab.bg(cx.theme().colors().drop_target_background)
2387            })
2388            .when_some(self.can_drop_predicate.clone(), |this, p| {
2389                this.can_drop(move |a, window, cx| p(a, window, cx))
2390            })
2391            .on_drop(
2392                cx.listener(move |this, dragged_tab: &DraggedTab, window, cx| {
2393                    this.drag_split_direction = None;
2394                    this.handle_tab_drop(dragged_tab, ix, window, cx)
2395                }),
2396            )
2397            .on_drop(
2398                cx.listener(move |this, selection: &DraggedSelection, window, cx| {
2399                    this.drag_split_direction = None;
2400                    this.handle_dragged_selection_drop(selection, Some(ix), window, cx)
2401                }),
2402            )
2403            .on_drop(cx.listener(move |this, paths, window, cx| {
2404                this.drag_split_direction = None;
2405                this.handle_external_paths_drop(paths, window, cx)
2406            }))
2407            .when_some(item.tab_tooltip_content(cx), |tab, content| match content {
2408                TabTooltipContent::Text(text) => tab.tooltip(Tooltip::text(text.clone())),
2409                TabTooltipContent::Custom(element_fn) => {
2410                    tab.tooltip(move |window, cx| element_fn(window, cx))
2411                }
2412            })
2413            .start_slot::<Indicator>(indicator)
2414            .map(|this| {
2415                let end_slot_action: &'static dyn Action;
2416                let end_slot_tooltip_text: &'static str;
2417                let end_slot = if is_pinned {
2418                    end_slot_action = &TogglePinTab;
2419                    end_slot_tooltip_text = "Unpin Tab";
2420                    IconButton::new("unpin tab", IconName::Pin)
2421                        .shape(IconButtonShape::Square)
2422                        .icon_color(Color::Muted)
2423                        .size(ButtonSize::None)
2424                        .icon_size(IconSize::XSmall)
2425                        .on_click(cx.listener(move |pane, _, window, cx| {
2426                            pane.unpin_tab_at(ix, window, cx);
2427                        }))
2428                } else {
2429                    end_slot_action = &CloseActiveItem {
2430                        save_intent: None,
2431                        close_pinned: false,
2432                    };
2433                    end_slot_tooltip_text = "Close Tab";
2434                    match show_close_button {
2435                        ShowCloseButton::Always => IconButton::new("close tab", IconName::Close),
2436                        ShowCloseButton::Hover => {
2437                            IconButton::new("close tab", IconName::Close).visible_on_hover("")
2438                        }
2439                        ShowCloseButton::Hidden => return this,
2440                    }
2441                    .shape(IconButtonShape::Square)
2442                    .icon_color(Color::Muted)
2443                    .size(ButtonSize::None)
2444                    .icon_size(IconSize::XSmall)
2445                    .on_click(cx.listener(move |pane, _, window, cx| {
2446                        pane.close_item_by_id(item_id, SaveIntent::Close, window, cx)
2447                            .detach_and_log_err(cx);
2448                    }))
2449                }
2450                .map(|this| {
2451                    if is_active {
2452                        let focus_handle = focus_handle.clone();
2453                        this.tooltip(move |window, cx| {
2454                            Tooltip::for_action_in(
2455                                end_slot_tooltip_text,
2456                                end_slot_action,
2457                                &focus_handle,
2458                                window,
2459                                cx,
2460                            )
2461                        })
2462                    } else {
2463                        this.tooltip(Tooltip::text(end_slot_tooltip_text))
2464                    }
2465                });
2466                this.end_slot(end_slot)
2467            })
2468            .child(
2469                h_flex()
2470                    .gap_1()
2471                    .items_center()
2472                    .children(
2473                        std::iter::once(if let Some(decorated_icon) = decorated_icon {
2474                            Some(div().child(decorated_icon.into_any_element()))
2475                        } else if let Some(icon) = icon {
2476                            Some(div().child(icon.into_any_element()))
2477                        } else {
2478                            None
2479                        })
2480                        .flatten(),
2481                    )
2482                    .child(label),
2483            );
2484
2485        let single_entry_to_resolve = self.items[ix]
2486            .is_singleton(cx)
2487            .then(|| self.items[ix].project_entry_ids(cx).get(0).copied())
2488            .flatten();
2489
2490        let total_items = self.items.len();
2491        let has_items_to_left = ix > 0;
2492        let has_items_to_right = ix < total_items - 1;
2493        let has_clean_items = self.items.iter().any(|item| !item.is_dirty(cx));
2494        let is_pinned = self.is_tab_pinned(ix);
2495        let pane = cx.entity().downgrade();
2496        let menu_context = item.item_focus_handle(cx);
2497        right_click_menu(ix)
2498            .trigger(|_| tab)
2499            .menu(move |window, cx| {
2500                let pane = pane.clone();
2501                let menu_context = menu_context.clone();
2502                ContextMenu::build(window, cx, move |mut menu, window, cx| {
2503                    let close_active_item_action = CloseActiveItem {
2504                        save_intent: None,
2505                        close_pinned: true,
2506                    };
2507                    let close_inactive_items_action = CloseInactiveItems {
2508                        save_intent: None,
2509                        close_pinned: false,
2510                    };
2511                    let close_items_to_the_left_action = CloseItemsToTheLeft {
2512                        close_pinned: false,
2513                    };
2514                    let close_items_to_the_right_action = CloseItemsToTheRight {
2515                        close_pinned: false,
2516                    };
2517                    let close_clean_items_action = CloseCleanItems {
2518                        close_pinned: false,
2519                    };
2520                    let close_all_items_action = CloseAllItems {
2521                        save_intent: None,
2522                        close_pinned: false,
2523                    };
2524                    if let Some(pane) = pane.upgrade() {
2525                        menu = menu
2526                            .entry(
2527                                "Close",
2528                                Some(Box::new(close_active_item_action)),
2529                                window.handler_for(&pane, move |pane, window, cx| {
2530                                    pane.close_item_by_id(item_id, SaveIntent::Close, window, cx)
2531                                        .detach_and_log_err(cx);
2532                                }),
2533                            )
2534                            .item(ContextMenuItem::Entry(
2535                                ContextMenuEntry::new("Close Others")
2536                                    .action(Box::new(close_inactive_items_action.clone()))
2537                                    .disabled(total_items == 1)
2538                                    .handler(window.handler_for(&pane, move |pane, window, cx| {
2539                                        pane.close_inactive_items(
2540                                            &close_inactive_items_action,
2541                                            window,
2542                                            cx,
2543                                        )
2544                                        .detach_and_log_err(cx);
2545                                    })),
2546                            ))
2547                            .separator()
2548                            .item(ContextMenuItem::Entry(
2549                                ContextMenuEntry::new("Close Left")
2550                                    .action(Box::new(close_items_to_the_left_action.clone()))
2551                                    .disabled(!has_items_to_left)
2552                                    .handler(window.handler_for(&pane, move |pane, window, cx| {
2553                                        pane.close_items_to_the_left_by_id(
2554                                            Some(item_id),
2555                                            &close_items_to_the_left_action,
2556                                            window,
2557                                            cx,
2558                                        )
2559                                        .detach_and_log_err(cx);
2560                                    })),
2561                            ))
2562                            .item(ContextMenuItem::Entry(
2563                                ContextMenuEntry::new("Close Right")
2564                                    .action(Box::new(close_items_to_the_right_action.clone()))
2565                                    .disabled(!has_items_to_right)
2566                                    .handler(window.handler_for(&pane, move |pane, window, cx| {
2567                                        pane.close_items_to_the_right_by_id(
2568                                            Some(item_id),
2569                                            &close_items_to_the_right_action,
2570                                            window,
2571                                            cx,
2572                                        )
2573                                        .detach_and_log_err(cx);
2574                                    })),
2575                            ))
2576                            .separator()
2577                            .item(ContextMenuItem::Entry(
2578                                ContextMenuEntry::new("Close Clean")
2579                                    .action(Box::new(close_clean_items_action.clone()))
2580                                    .disabled(!has_clean_items)
2581                                    .handler(window.handler_for(&pane, move |pane, window, cx| {
2582                                        pane.close_clean_items(
2583                                            &close_clean_items_action,
2584                                            window,
2585                                            cx,
2586                                        )
2587                                        .detach_and_log_err(cx)
2588                                    })),
2589                            ))
2590                            .entry(
2591                                "Close All",
2592                                Some(Box::new(close_all_items_action.clone())),
2593                                window.handler_for(&pane, move |pane, window, cx| {
2594                                    pane.close_all_items(&close_all_items_action, window, cx)
2595                                        .detach_and_log_err(cx)
2596                                }),
2597                            );
2598
2599                        let pin_tab_entries = |menu: ContextMenu| {
2600                            menu.separator().map(|this| {
2601                                if is_pinned {
2602                                    this.entry(
2603                                        "Unpin Tab",
2604                                        Some(TogglePinTab.boxed_clone()),
2605                                        window.handler_for(&pane, move |pane, window, cx| {
2606                                            pane.unpin_tab_at(ix, window, cx);
2607                                        }),
2608                                    )
2609                                } else {
2610                                    this.entry(
2611                                        "Pin Tab",
2612                                        Some(TogglePinTab.boxed_clone()),
2613                                        window.handler_for(&pane, move |pane, window, cx| {
2614                                            pane.pin_tab_at(ix, window, cx);
2615                                        }),
2616                                    )
2617                                }
2618                            })
2619                        };
2620                        if let Some(entry) = single_entry_to_resolve {
2621                            let project_path = pane
2622                                .read(cx)
2623                                .item_for_entry(entry, cx)
2624                                .and_then(|item| item.project_path(cx));
2625                            let worktree = project_path.as_ref().and_then(|project_path| {
2626                                pane.read(cx)
2627                                    .project
2628                                    .upgrade()?
2629                                    .read(cx)
2630                                    .worktree_for_id(project_path.worktree_id, cx)
2631                            });
2632                            let has_relative_path = worktree.as_ref().is_some_and(|worktree| {
2633                                worktree
2634                                    .read(cx)
2635                                    .root_entry()
2636                                    .map_or(false, |entry| entry.is_dir())
2637                            });
2638
2639                            let entry_abs_path = pane.read(cx).entry_abs_path(entry, cx);
2640                            let parent_abs_path = entry_abs_path
2641                                .as_deref()
2642                                .and_then(|abs_path| Some(abs_path.parent()?.to_path_buf()));
2643                            let relative_path = project_path
2644                                .map(|project_path| project_path.path)
2645                                .filter(|_| has_relative_path);
2646
2647                            let visible_in_project_panel = relative_path.is_some()
2648                                && worktree.is_some_and(|worktree| worktree.read(cx).is_visible());
2649
2650                            let entry_id = entry.to_proto();
2651                            menu = menu
2652                                .separator()
2653                                .when_some(entry_abs_path, |menu, abs_path| {
2654                                    menu.entry(
2655                                        "Copy Path",
2656                                        Some(Box::new(zed_actions::workspace::CopyPath)),
2657                                        window.handler_for(&pane, move |_, _, cx| {
2658                                            cx.write_to_clipboard(ClipboardItem::new_string(
2659                                                abs_path.to_string_lossy().to_string(),
2660                                            ));
2661                                        }),
2662                                    )
2663                                })
2664                                .when_some(relative_path, |menu, relative_path| {
2665                                    menu.entry(
2666                                        "Copy Relative Path",
2667                                        Some(Box::new(zed_actions::workspace::CopyRelativePath)),
2668                                        window.handler_for(&pane, move |_, _, cx| {
2669                                            cx.write_to_clipboard(ClipboardItem::new_string(
2670                                                relative_path.to_string_lossy().to_string(),
2671                                            ));
2672                                        }),
2673                                    )
2674                                })
2675                                .map(pin_tab_entries)
2676                                .separator()
2677                                .when(visible_in_project_panel, |menu| {
2678                                    menu.entry(
2679                                        "Reveal In Project Panel",
2680                                        Some(Box::new(RevealInProjectPanel {
2681                                            entry_id: Some(entry_id),
2682                                        })),
2683                                        window.handler_for(&pane, move |pane, _, cx| {
2684                                            pane.project
2685                                                .update(cx, |_, cx| {
2686                                                    cx.emit(project::Event::RevealInProjectPanel(
2687                                                        ProjectEntryId::from_proto(entry_id),
2688                                                    ))
2689                                                })
2690                                                .ok();
2691                                        }),
2692                                    )
2693                                })
2694                                .when_some(parent_abs_path, |menu, parent_abs_path| {
2695                                    menu.entry(
2696                                        "Open in Terminal",
2697                                        Some(Box::new(OpenInTerminal)),
2698                                        window.handler_for(&pane, move |_, window, cx| {
2699                                            window.dispatch_action(
2700                                                OpenTerminal {
2701                                                    working_directory: parent_abs_path.clone(),
2702                                                }
2703                                                .boxed_clone(),
2704                                                cx,
2705                                            );
2706                                        }),
2707                                    )
2708                                });
2709                        } else {
2710                            menu = menu.map(pin_tab_entries);
2711                        }
2712                    }
2713
2714                    menu.context(menu_context)
2715                })
2716            })
2717    }
2718
2719    fn render_tab_bar(&mut self, window: &mut Window, cx: &mut Context<Pane>) -> AnyElement {
2720        let focus_handle = self.focus_handle.clone();
2721        let navigate_backward = IconButton::new("navigate_backward", IconName::ArrowLeft)
2722            .icon_size(IconSize::Small)
2723            .on_click({
2724                let entity = cx.entity().clone();
2725                move |_, window, cx| {
2726                    entity.update(cx, |pane, cx| pane.navigate_backward(window, cx))
2727                }
2728            })
2729            .disabled(!self.can_navigate_backward())
2730            .tooltip({
2731                let focus_handle = focus_handle.clone();
2732                move |window, cx| {
2733                    Tooltip::for_action_in("Go Back", &GoBack, &focus_handle, window, cx)
2734                }
2735            });
2736
2737        let navigate_forward = IconButton::new("navigate_forward", IconName::ArrowRight)
2738            .icon_size(IconSize::Small)
2739            .on_click({
2740                let entity = cx.entity().clone();
2741                move |_, window, cx| entity.update(cx, |pane, cx| pane.navigate_forward(window, cx))
2742            })
2743            .disabled(!self.can_navigate_forward())
2744            .tooltip({
2745                let focus_handle = focus_handle.clone();
2746                move |window, cx| {
2747                    Tooltip::for_action_in("Go Forward", &GoForward, &focus_handle, window, cx)
2748                }
2749            });
2750
2751        let mut tab_items = self
2752            .items
2753            .iter()
2754            .enumerate()
2755            .zip(tab_details(&self.items, window, cx))
2756            .map(|((ix, item), detail)| {
2757                self.render_tab(ix, &**item, detail, &focus_handle, window, cx)
2758            })
2759            .collect::<Vec<_>>();
2760        let tab_count = tab_items.len();
2761        let unpinned_tabs = tab_items.split_off(self.pinned_tab_count);
2762        let pinned_tabs = tab_items;
2763        TabBar::new("tab_bar")
2764            .when(
2765                self.display_nav_history_buttons.unwrap_or_default(),
2766                |tab_bar| {
2767                    tab_bar
2768                        .start_child(navigate_backward)
2769                        .start_child(navigate_forward)
2770                },
2771            )
2772            .map(|tab_bar| {
2773                if self.show_tab_bar_buttons {
2774                    let render_tab_buttons = self.render_tab_bar_buttons.clone();
2775                    let (left_children, right_children) = render_tab_buttons(self, window, cx);
2776                    tab_bar
2777                        .start_children(left_children)
2778                        .end_children(right_children)
2779                } else {
2780                    tab_bar
2781                }
2782            })
2783            .children(pinned_tabs.len().ne(&0).then(|| {
2784                let content_width = self.tab_bar_scroll_handle.content_size().width;
2785                let viewport_width = self.tab_bar_scroll_handle.viewport().size.width;
2786                // We need to check both because offset returns delta values even when the scroll handle is not scrollable
2787                let is_scrollable = content_width > viewport_width;
2788                let is_scrolled = self.tab_bar_scroll_handle.offset().x < px(0.);
2789                let has_active_unpinned_tab = self.active_item_index >= self.pinned_tab_count;
2790                h_flex()
2791                    .children(pinned_tabs)
2792                    .when(is_scrollable && is_scrolled, |this| {
2793                        this.when(has_active_unpinned_tab, |this| this.border_r_2())
2794                            .when(!has_active_unpinned_tab, |this| this.border_r_1())
2795                            .border_color(cx.theme().colors().border)
2796                    })
2797            }))
2798            .child(
2799                h_flex()
2800                    .id("unpinned tabs")
2801                    .overflow_x_scroll()
2802                    .w_full()
2803                    .track_scroll(&self.tab_bar_scroll_handle)
2804                    .children(unpinned_tabs)
2805                    .child(
2806                        div()
2807                            .id("tab_bar_drop_target")
2808                            .min_w_6()
2809                            // HACK: This empty child is currently necessary to force the drop target to appear
2810                            // despite us setting a min width above.
2811                            .child("")
2812                            .h_full()
2813                            .flex_grow()
2814                            .drag_over::<DraggedTab>(|bar, _, _, cx| {
2815                                bar.bg(cx.theme().colors().drop_target_background)
2816                            })
2817                            .drag_over::<DraggedSelection>(|bar, _, _, cx| {
2818                                bar.bg(cx.theme().colors().drop_target_background)
2819                            })
2820                            .on_drop(cx.listener(
2821                                move |this, dragged_tab: &DraggedTab, window, cx| {
2822                                    this.drag_split_direction = None;
2823                                    this.handle_tab_drop(dragged_tab, this.items.len(), window, cx)
2824                                },
2825                            ))
2826                            .on_drop(cx.listener(
2827                                move |this, selection: &DraggedSelection, window, cx| {
2828                                    this.drag_split_direction = None;
2829                                    this.handle_project_entry_drop(
2830                                        &selection.active_selection.entry_id,
2831                                        Some(tab_count),
2832                                        window,
2833                                        cx,
2834                                    )
2835                                },
2836                            ))
2837                            .on_drop(cx.listener(move |this, paths, window, cx| {
2838                                this.drag_split_direction = None;
2839                                this.handle_external_paths_drop(paths, window, cx)
2840                            }))
2841                            .on_click(cx.listener(move |this, event: &ClickEvent, window, cx| {
2842                                if event.up.click_count == 2 {
2843                                    window.dispatch_action(
2844                                        this.double_click_dispatch_action.boxed_clone(),
2845                                        cx,
2846                                    );
2847                                }
2848                            })),
2849                    ),
2850            )
2851            .into_any_element()
2852    }
2853
2854    pub fn render_menu_overlay(menu: &Entity<ContextMenu>) -> Div {
2855        div().absolute().bottom_0().right_0().size_0().child(
2856            deferred(anchored().anchor(Corner::TopRight).child(menu.clone())).with_priority(1),
2857        )
2858    }
2859
2860    pub fn set_zoomed(&mut self, zoomed: bool, cx: &mut Context<Self>) {
2861        self.zoomed = zoomed;
2862        cx.notify();
2863    }
2864
2865    pub fn is_zoomed(&self) -> bool {
2866        self.zoomed
2867    }
2868
2869    fn handle_drag_move<T: 'static>(
2870        &mut self,
2871        event: &DragMoveEvent<T>,
2872        window: &mut Window,
2873        cx: &mut Context<Self>,
2874    ) {
2875        let can_split_predicate = self.can_split_predicate.take();
2876        let can_split = match &can_split_predicate {
2877            Some(can_split_predicate) => {
2878                can_split_predicate(self, event.dragged_item(), window, cx)
2879            }
2880            None => false,
2881        };
2882        self.can_split_predicate = can_split_predicate;
2883        if !can_split {
2884            return;
2885        }
2886
2887        let rect = event.bounds.size;
2888
2889        let size = event.bounds.size.width.min(event.bounds.size.height)
2890            * WorkspaceSettings::get_global(cx).drop_target_size;
2891
2892        let relative_cursor = Point::new(
2893            event.event.position.x - event.bounds.left(),
2894            event.event.position.y - event.bounds.top(),
2895        );
2896
2897        let direction = if relative_cursor.x < size
2898            || relative_cursor.x > rect.width - size
2899            || relative_cursor.y < size
2900            || relative_cursor.y > rect.height - size
2901        {
2902            [
2903                SplitDirection::Up,
2904                SplitDirection::Right,
2905                SplitDirection::Down,
2906                SplitDirection::Left,
2907            ]
2908            .iter()
2909            .min_by_key(|side| match side {
2910                SplitDirection::Up => relative_cursor.y,
2911                SplitDirection::Right => rect.width - relative_cursor.x,
2912                SplitDirection::Down => rect.height - relative_cursor.y,
2913                SplitDirection::Left => relative_cursor.x,
2914            })
2915            .cloned()
2916        } else {
2917            None
2918        };
2919
2920        if direction != self.drag_split_direction {
2921            self.drag_split_direction = direction;
2922        }
2923    }
2924
2925    pub fn handle_tab_drop(
2926        &mut self,
2927        dragged_tab: &DraggedTab,
2928        ix: usize,
2929        window: &mut Window,
2930        cx: &mut Context<Self>,
2931    ) {
2932        if let Some(custom_drop_handle) = self.custom_drop_handle.clone() {
2933            if let ControlFlow::Break(()) = custom_drop_handle(self, dragged_tab, window, cx) {
2934                return;
2935            }
2936        }
2937        let mut to_pane = cx.entity().clone();
2938        let split_direction = self.drag_split_direction;
2939        let item_id = dragged_tab.item.item_id();
2940        if let Some(preview_item_id) = self.preview_item_id {
2941            if item_id == preview_item_id {
2942                self.set_preview_item_id(None, cx);
2943            }
2944        }
2945
2946        let is_clone = cfg!(target_os = "macos") && window.modifiers().alt
2947            || cfg!(not(target_os = "macos")) && window.modifiers().control;
2948
2949        let from_pane = dragged_tab.pane.clone();
2950        let from_ix = dragged_tab.ix;
2951        self.workspace
2952            .update(cx, |_, cx| {
2953                cx.defer_in(window, move |workspace, window, cx| {
2954                    if let Some(split_direction) = split_direction {
2955                        to_pane = workspace.split_pane(to_pane, split_direction, window, cx);
2956                    }
2957                    let database_id = workspace.database_id();
2958                    let was_pinned_in_from_pane = from_pane.read_with(cx, |pane, _| {
2959                        pane.index_for_item_id(item_id)
2960                            .is_some_and(|ix| pane.is_tab_pinned(ix))
2961                    });
2962                    let to_pane_old_length = to_pane.read(cx).items.len();
2963                    if is_clone {
2964                        let Some(item) = from_pane
2965                            .read(cx)
2966                            .items()
2967                            .find(|item| item.item_id() == item_id)
2968                            .map(|item| item.clone())
2969                        else {
2970                            return;
2971                        };
2972                        if let Some(item) = item.clone_on_split(database_id, window, cx) {
2973                            to_pane.update(cx, |pane, cx| {
2974                                pane.add_item(item, true, true, None, window, cx);
2975                            })
2976                        }
2977                    } else {
2978                        move_item(&from_pane, &to_pane, item_id, ix, true, window, cx);
2979                    }
2980                    to_pane.update(cx, |this, _| {
2981                        if to_pane == from_pane {
2982                            let moved_right = ix > from_ix;
2983                            let ix = if moved_right { ix - 1 } else { ix };
2984                            let is_pinned_in_to_pane = this.is_tab_pinned(ix);
2985
2986                            if !was_pinned_in_from_pane && is_pinned_in_to_pane {
2987                                this.pinned_tab_count += 1;
2988                            } else if was_pinned_in_from_pane && !is_pinned_in_to_pane {
2989                                this.pinned_tab_count -= 1;
2990                            }
2991                        } else if this.items.len() >= to_pane_old_length {
2992                            let is_pinned_in_to_pane = this.is_tab_pinned(ix);
2993                            let item_created_pane = to_pane_old_length == 0;
2994                            let is_first_position = ix == 0;
2995                            let was_dropped_at_beginning = item_created_pane || is_first_position;
2996                            let should_remain_pinned = is_pinned_in_to_pane
2997                                || (was_pinned_in_from_pane && was_dropped_at_beginning);
2998
2999                            if should_remain_pinned {
3000                                this.pinned_tab_count += 1;
3001                            }
3002                        }
3003                    });
3004                });
3005            })
3006            .log_err();
3007    }
3008
3009    fn handle_dragged_selection_drop(
3010        &mut self,
3011        dragged_selection: &DraggedSelection,
3012        dragged_onto: Option<usize>,
3013        window: &mut Window,
3014        cx: &mut Context<Self>,
3015    ) {
3016        if let Some(custom_drop_handle) = self.custom_drop_handle.clone() {
3017            if let ControlFlow::Break(()) = custom_drop_handle(self, dragged_selection, window, cx)
3018            {
3019                return;
3020            }
3021        }
3022        self.handle_project_entry_drop(
3023            &dragged_selection.active_selection.entry_id,
3024            dragged_onto,
3025            window,
3026            cx,
3027        );
3028    }
3029
3030    fn handle_project_entry_drop(
3031        &mut self,
3032        project_entry_id: &ProjectEntryId,
3033        target: Option<usize>,
3034        window: &mut Window,
3035        cx: &mut Context<Self>,
3036    ) {
3037        if let Some(custom_drop_handle) = self.custom_drop_handle.clone() {
3038            if let ControlFlow::Break(()) = custom_drop_handle(self, project_entry_id, window, cx) {
3039                return;
3040            }
3041        }
3042        let mut to_pane = cx.entity().clone();
3043        let split_direction = self.drag_split_direction;
3044        let project_entry_id = *project_entry_id;
3045        self.workspace
3046            .update(cx, |_, cx| {
3047                cx.defer_in(window, move |workspace, window, cx| {
3048                    if let Some(project_path) = workspace
3049                        .project()
3050                        .read(cx)
3051                        .path_for_entry(project_entry_id, cx)
3052                    {
3053                        let load_path_task = workspace.load_path(project_path.clone(), window, cx);
3054                        cx.spawn_in(window, async move |workspace, cx| {
3055                            if let Some((project_entry_id, build_item)) =
3056                                load_path_task.await.notify_async_err(cx)
3057                            {
3058                                let (to_pane, new_item_handle) = workspace
3059                                    .update_in(cx, |workspace, window, cx| {
3060                                        if let Some(split_direction) = split_direction {
3061                                            to_pane = workspace.split_pane(
3062                                                to_pane,
3063                                                split_direction,
3064                                                window,
3065                                                cx,
3066                                            );
3067                                        }
3068                                        let new_item_handle = to_pane.update(cx, |pane, cx| {
3069                                            pane.open_item(
3070                                                project_entry_id,
3071                                                project_path,
3072                                                true,
3073                                                false,
3074                                                true,
3075                                                target,
3076                                                window,
3077                                                cx,
3078                                                build_item,
3079                                            )
3080                                        });
3081                                        (to_pane, new_item_handle)
3082                                    })
3083                                    .log_err()?;
3084                                to_pane
3085                                    .update_in(cx, |this, window, cx| {
3086                                        let Some(index) = this.index_for_item(&*new_item_handle)
3087                                        else {
3088                                            return;
3089                                        };
3090
3091                                        if target.map_or(false, |target| this.is_tab_pinned(target))
3092                                        {
3093                                            this.pin_tab_at(index, window, cx);
3094                                        }
3095                                    })
3096                                    .ok()?
3097                            }
3098                            Some(())
3099                        })
3100                        .detach();
3101                    };
3102                });
3103            })
3104            .log_err();
3105    }
3106
3107    fn handle_external_paths_drop(
3108        &mut self,
3109        paths: &ExternalPaths,
3110        window: &mut Window,
3111        cx: &mut Context<Self>,
3112    ) {
3113        if let Some(custom_drop_handle) = self.custom_drop_handle.clone() {
3114            if let ControlFlow::Break(()) = custom_drop_handle(self, paths, window, cx) {
3115                return;
3116            }
3117        }
3118        let mut to_pane = cx.entity().clone();
3119        let mut split_direction = self.drag_split_direction;
3120        let paths = paths.paths().to_vec();
3121        let is_remote = self
3122            .workspace
3123            .update(cx, |workspace, cx| {
3124                if workspace.project().read(cx).is_via_collab() {
3125                    workspace.show_error(
3126                        &anyhow::anyhow!("Cannot drop files on a remote project"),
3127                        cx,
3128                    );
3129                    true
3130                } else {
3131                    false
3132                }
3133            })
3134            .unwrap_or(true);
3135        if is_remote {
3136            return;
3137        }
3138
3139        self.workspace
3140            .update(cx, |workspace, cx| {
3141                let fs = Arc::clone(workspace.project().read(cx).fs());
3142                cx.spawn_in(window, async move |workspace, cx| {
3143                    let mut is_file_checks = FuturesUnordered::new();
3144                    for path in &paths {
3145                        is_file_checks.push(fs.is_file(path))
3146                    }
3147                    let mut has_files_to_open = false;
3148                    while let Some(is_file) = is_file_checks.next().await {
3149                        if is_file {
3150                            has_files_to_open = true;
3151                            break;
3152                        }
3153                    }
3154                    drop(is_file_checks);
3155                    if !has_files_to_open {
3156                        split_direction = None;
3157                    }
3158
3159                    if let Ok(open_task) = workspace.update_in(cx, |workspace, window, cx| {
3160                        if let Some(split_direction) = split_direction {
3161                            to_pane = workspace.split_pane(to_pane, split_direction, window, cx);
3162                        }
3163                        workspace.open_paths(
3164                            paths,
3165                            OpenOptions {
3166                                visible: Some(OpenVisible::OnlyDirectories),
3167                                ..Default::default()
3168                            },
3169                            Some(to_pane.downgrade()),
3170                            window,
3171                            cx,
3172                        )
3173                    }) {
3174                        let opened_items: Vec<_> = open_task.await;
3175                        _ = workspace.update(cx, |workspace, cx| {
3176                            for item in opened_items.into_iter().flatten() {
3177                                if let Err(e) = item {
3178                                    workspace.show_error(&e, cx);
3179                                }
3180                            }
3181                        });
3182                    }
3183                })
3184                .detach();
3185            })
3186            .log_err();
3187    }
3188
3189    pub fn display_nav_history_buttons(&mut self, display: Option<bool>) {
3190        self.display_nav_history_buttons = display;
3191    }
3192
3193    fn pinned_item_ids(&self) -> Vec<EntityId> {
3194        self.items
3195            .iter()
3196            .enumerate()
3197            .filter_map(|(index, item)| {
3198                if self.is_tab_pinned(index) {
3199                    return Some(item.item_id());
3200                }
3201
3202                None
3203            })
3204            .collect()
3205    }
3206
3207    fn clean_item_ids(&self, cx: &mut Context<Pane>) -> Vec<EntityId> {
3208        self.items()
3209            .filter_map(|item| {
3210                if !item.is_dirty(cx) {
3211                    return Some(item.item_id());
3212                }
3213
3214                None
3215            })
3216            .collect()
3217    }
3218
3219    fn to_the_side_item_ids(&self, item_id: EntityId, side: Side) -> Vec<EntityId> {
3220        match side {
3221            Side::Left => self
3222                .items()
3223                .take_while(|item| item.item_id() != item_id)
3224                .map(|item| item.item_id())
3225                .collect(),
3226            Side::Right => self
3227                .items()
3228                .rev()
3229                .take_while(|item| item.item_id() != item_id)
3230                .map(|item| item.item_id())
3231                .collect(),
3232        }
3233    }
3234
3235    pub fn drag_split_direction(&self) -> Option<SplitDirection> {
3236        self.drag_split_direction
3237    }
3238
3239    pub fn set_zoom_out_on_close(&mut self, zoom_out_on_close: bool) {
3240        self.zoom_out_on_close = zoom_out_on_close;
3241    }
3242}
3243
3244fn default_render_tab_bar_buttons(
3245    pane: &mut Pane,
3246    window: &mut Window,
3247    cx: &mut Context<Pane>,
3248) -> (Option<AnyElement>, Option<AnyElement>) {
3249    if !pane.has_focus(window, cx) && !pane.context_menu_focused(window, cx) {
3250        return (None, None);
3251    }
3252    // Ideally we would return a vec of elements here to pass directly to the [TabBar]'s
3253    // `end_slot`, but due to needing a view here that isn't possible.
3254    let right_children = h_flex()
3255        // Instead we need to replicate the spacing from the [TabBar]'s `end_slot` here.
3256        .gap(DynamicSpacing::Base04.rems(cx))
3257        .child(
3258            PopoverMenu::new("pane-tab-bar-popover-menu")
3259                .trigger_with_tooltip(
3260                    IconButton::new("plus", IconName::Plus).icon_size(IconSize::Small),
3261                    Tooltip::text("New..."),
3262                )
3263                .anchor(Corner::TopRight)
3264                .with_handle(pane.new_item_context_menu_handle.clone())
3265                .menu(move |window, cx| {
3266                    Some(ContextMenu::build(window, cx, |menu, _, _| {
3267                        menu.action("New File", NewFile.boxed_clone())
3268                            .action("Open File", ToggleFileFinder::default().boxed_clone())
3269                            .separator()
3270                            .action(
3271                                "Search Project",
3272                                DeploySearch {
3273                                    replace_enabled: false,
3274                                    included_files: None,
3275                                    excluded_files: None,
3276                                }
3277                                .boxed_clone(),
3278                            )
3279                            .action("Search Symbols", ToggleProjectSymbols.boxed_clone())
3280                            .separator()
3281                            .action("New Terminal", NewTerminal.boxed_clone())
3282                    }))
3283                }),
3284        )
3285        .child(
3286            PopoverMenu::new("pane-tab-bar-split")
3287                .trigger_with_tooltip(
3288                    IconButton::new("split", IconName::Split).icon_size(IconSize::Small),
3289                    Tooltip::text("Split Pane"),
3290                )
3291                .anchor(Corner::TopRight)
3292                .with_handle(pane.split_item_context_menu_handle.clone())
3293                .menu(move |window, cx| {
3294                    ContextMenu::build(window, cx, |menu, _, _| {
3295                        menu.action("Split Right", SplitRight.boxed_clone())
3296                            .action("Split Left", SplitLeft.boxed_clone())
3297                            .action("Split Up", SplitUp.boxed_clone())
3298                            .action("Split Down", SplitDown.boxed_clone())
3299                    })
3300                    .into()
3301                }),
3302        )
3303        .child({
3304            let zoomed = pane.is_zoomed();
3305            IconButton::new("toggle_zoom", IconName::Maximize)
3306                .icon_size(IconSize::Small)
3307                .toggle_state(zoomed)
3308                .selected_icon(IconName::Minimize)
3309                .on_click(cx.listener(|pane, _, window, cx| {
3310                    pane.toggle_zoom(&crate::ToggleZoom, window, cx);
3311                }))
3312                .tooltip(move |window, cx| {
3313                    Tooltip::for_action(
3314                        if zoomed { "Zoom Out" } else { "Zoom In" },
3315                        &ToggleZoom,
3316                        window,
3317                        cx,
3318                    )
3319                })
3320        })
3321        .into_any_element()
3322        .into();
3323    (None, right_children)
3324}
3325
3326impl Focusable for Pane {
3327    fn focus_handle(&self, _cx: &App) -> FocusHandle {
3328        self.focus_handle.clone()
3329    }
3330}
3331
3332impl Render for Pane {
3333    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
3334        let mut key_context = KeyContext::new_with_defaults();
3335        key_context.add("Pane");
3336        if self.active_item().is_none() {
3337            key_context.add("EmptyPane");
3338        }
3339
3340        let should_display_tab_bar = self.should_display_tab_bar.clone();
3341        let display_tab_bar = should_display_tab_bar(window, cx);
3342        let Some(project) = self.project.upgrade() else {
3343            return div().track_focus(&self.focus_handle(cx));
3344        };
3345        let is_local = project.read(cx).is_local();
3346
3347        v_flex()
3348            .key_context(key_context)
3349            .track_focus(&self.focus_handle(cx))
3350            .size_full()
3351            .flex_none()
3352            .overflow_hidden()
3353            .on_action(cx.listener(|pane, _: &AlternateFile, window, cx| {
3354                pane.alternate_file(window, cx);
3355            }))
3356            .on_action(
3357                cx.listener(|pane, _: &SplitLeft, _, cx| pane.split(SplitDirection::Left, cx)),
3358            )
3359            .on_action(cx.listener(|pane, _: &SplitUp, _, cx| pane.split(SplitDirection::Up, cx)))
3360            .on_action(cx.listener(|pane, _: &SplitHorizontal, _, cx| {
3361                pane.split(SplitDirection::horizontal(cx), cx)
3362            }))
3363            .on_action(cx.listener(|pane, _: &SplitVertical, _, cx| {
3364                pane.split(SplitDirection::vertical(cx), cx)
3365            }))
3366            .on_action(
3367                cx.listener(|pane, _: &SplitRight, _, cx| pane.split(SplitDirection::Right, cx)),
3368            )
3369            .on_action(
3370                cx.listener(|pane, _: &SplitDown, _, cx| pane.split(SplitDirection::Down, cx)),
3371            )
3372            .on_action(
3373                cx.listener(|pane, _: &GoBack, window, cx| pane.navigate_backward(window, cx)),
3374            )
3375            .on_action(
3376                cx.listener(|pane, _: &GoForward, window, cx| pane.navigate_forward(window, cx)),
3377            )
3378            .on_action(cx.listener(|_, _: &JoinIntoNext, _, cx| {
3379                cx.emit(Event::JoinIntoNext);
3380            }))
3381            .on_action(cx.listener(|_, _: &JoinAll, _, cx| {
3382                cx.emit(Event::JoinAll);
3383            }))
3384            .on_action(cx.listener(Pane::toggle_zoom))
3385            .on_action(
3386                cx.listener(|pane: &mut Pane, action: &ActivateItem, window, cx| {
3387                    pane.activate_item(
3388                        action.0.min(pane.items.len().saturating_sub(1)),
3389                        true,
3390                        true,
3391                        window,
3392                        cx,
3393                    );
3394                }),
3395            )
3396            .on_action(
3397                cx.listener(|pane: &mut Pane, _: &ActivateLastItem, window, cx| {
3398                    pane.activate_item(pane.items.len().saturating_sub(1), true, true, window, cx);
3399                }),
3400            )
3401            .on_action(
3402                cx.listener(|pane: &mut Pane, _: &ActivatePreviousItem, window, cx| {
3403                    pane.activate_prev_item(true, window, cx);
3404                }),
3405            )
3406            .on_action(
3407                cx.listener(|pane: &mut Pane, _: &ActivateNextItem, window, cx| {
3408                    pane.activate_next_item(true, window, cx);
3409                }),
3410            )
3411            .on_action(
3412                cx.listener(|pane, _: &SwapItemLeft, window, cx| pane.swap_item_left(window, cx)),
3413            )
3414            .on_action(
3415                cx.listener(|pane, _: &SwapItemRight, window, cx| pane.swap_item_right(window, cx)),
3416            )
3417            .on_action(cx.listener(|pane, action, window, cx| {
3418                pane.toggle_pin_tab(action, window, cx);
3419            }))
3420            .on_action(cx.listener(|pane, action, window, cx| {
3421                pane.unpin_all_tabs(action, window, cx);
3422            }))
3423            .when(PreviewTabsSettings::get_global(cx).enabled, |this| {
3424                this.on_action(cx.listener(|pane: &mut Pane, _: &TogglePreviewTab, _, cx| {
3425                    if let Some(active_item_id) = pane.active_item().map(|i| i.item_id()) {
3426                        if pane.is_active_preview_item(active_item_id) {
3427                            pane.set_preview_item_id(None, cx);
3428                        } else {
3429                            pane.set_preview_item_id(Some(active_item_id), cx);
3430                        }
3431                    }
3432                }))
3433            })
3434            .on_action(
3435                cx.listener(|pane: &mut Self, action: &CloseActiveItem, window, cx| {
3436                    pane.close_active_item(action, window, cx)
3437                        .detach_and_log_err(cx)
3438                }),
3439            )
3440            .on_action(
3441                cx.listener(|pane: &mut Self, action: &CloseInactiveItems, window, cx| {
3442                    pane.close_inactive_items(action, window, cx)
3443                        .detach_and_log_err(cx);
3444                }),
3445            )
3446            .on_action(
3447                cx.listener(|pane: &mut Self, action: &CloseCleanItems, window, cx| {
3448                    pane.close_clean_items(action, window, cx)
3449                        .detach_and_log_err(cx)
3450                }),
3451            )
3452            .on_action(cx.listener(
3453                |pane: &mut Self, action: &CloseItemsToTheLeft, window, cx| {
3454                    pane.close_items_to_the_left_by_id(None, action, window, cx)
3455                        .detach_and_log_err(cx)
3456                },
3457            ))
3458            .on_action(cx.listener(
3459                |pane: &mut Self, action: &CloseItemsToTheRight, window, cx| {
3460                    pane.close_items_to_the_right_by_id(None, action, window, cx)
3461                        .detach_and_log_err(cx)
3462                },
3463            ))
3464            .on_action(
3465                cx.listener(|pane: &mut Self, action: &CloseAllItems, window, cx| {
3466                    pane.close_all_items(action, window, cx)
3467                        .detach_and_log_err(cx)
3468                }),
3469            )
3470            .on_action(
3471                cx.listener(|pane: &mut Self, action: &RevealInProjectPanel, _, cx| {
3472                    let entry_id = action
3473                        .entry_id
3474                        .map(ProjectEntryId::from_proto)
3475                        .or_else(|| pane.active_item()?.project_entry_ids(cx).first().copied());
3476                    if let Some(entry_id) = entry_id {
3477                        pane.project
3478                            .update(cx, |_, cx| {
3479                                cx.emit(project::Event::RevealInProjectPanel(entry_id))
3480                            })
3481                            .ok();
3482                    }
3483                }),
3484            )
3485            .on_action(cx.listener(|_, _: &menu::Cancel, window, cx| {
3486                if cx.stop_active_drag(window) {
3487                    return;
3488                } else {
3489                    cx.propagate();
3490                }
3491            }))
3492            .when(self.active_item().is_some() && display_tab_bar, |pane| {
3493                pane.child((self.render_tab_bar.clone())(self, window, cx))
3494            })
3495            .child({
3496                let has_worktrees = project.read(cx).visible_worktrees(cx).next().is_some();
3497                // main content
3498                div()
3499                    .flex_1()
3500                    .relative()
3501                    .group("")
3502                    .overflow_hidden()
3503                    .on_drag_move::<DraggedTab>(cx.listener(Self::handle_drag_move))
3504                    .on_drag_move::<DraggedSelection>(cx.listener(Self::handle_drag_move))
3505                    .when(is_local, |div| {
3506                        div.on_drag_move::<ExternalPaths>(cx.listener(Self::handle_drag_move))
3507                    })
3508                    .map(|div| {
3509                        if let Some(item) = self.active_item() {
3510                            div.id("pane_placeholder")
3511                                .v_flex()
3512                                .size_full()
3513                                .overflow_hidden()
3514                                .child(self.toolbar.clone())
3515                                .child(item.to_any())
3516                        } else {
3517                            let placeholder = div
3518                                .id("pane_placeholder")
3519                                .h_flex()
3520                                .size_full()
3521                                .justify_center()
3522                                .on_click(cx.listener(
3523                                    move |this, event: &ClickEvent, window, cx| {
3524                                        if event.up.click_count == 2 {
3525                                            window.dispatch_action(
3526                                                this.double_click_dispatch_action.boxed_clone(),
3527                                                cx,
3528                                            );
3529                                        }
3530                                    },
3531                                ));
3532                            if has_worktrees {
3533                                placeholder
3534                            } else {
3535                                placeholder.child(
3536                                    Label::new("Open a file or project to get started.")
3537                                        .color(Color::Muted),
3538                                )
3539                            }
3540                        }
3541                    })
3542                    .child(
3543                        // drag target
3544                        div()
3545                            .invisible()
3546                            .absolute()
3547                            .bg(cx.theme().colors().drop_target_background)
3548                            .group_drag_over::<DraggedTab>("", |style| style.visible())
3549                            .group_drag_over::<DraggedSelection>("", |style| style.visible())
3550                            .when(is_local, |div| {
3551                                div.group_drag_over::<ExternalPaths>("", |style| style.visible())
3552                            })
3553                            .when_some(self.can_drop_predicate.clone(), |this, p| {
3554                                this.can_drop(move |a, window, cx| p(a, window, cx))
3555                            })
3556                            .on_drop(cx.listener(move |this, dragged_tab, window, cx| {
3557                                this.handle_tab_drop(
3558                                    dragged_tab,
3559                                    this.active_item_index(),
3560                                    window,
3561                                    cx,
3562                                )
3563                            }))
3564                            .on_drop(cx.listener(
3565                                move |this, selection: &DraggedSelection, window, cx| {
3566                                    this.handle_dragged_selection_drop(selection, None, window, cx)
3567                                },
3568                            ))
3569                            .on_drop(cx.listener(move |this, paths, window, cx| {
3570                                this.handle_external_paths_drop(paths, window, cx)
3571                            }))
3572                            .map(|div| {
3573                                let size = DefiniteLength::Fraction(0.5);
3574                                match self.drag_split_direction {
3575                                    None => div.top_0().right_0().bottom_0().left_0(),
3576                                    Some(SplitDirection::Up) => {
3577                                        div.top_0().left_0().right_0().h(size)
3578                                    }
3579                                    Some(SplitDirection::Down) => {
3580                                        div.left_0().bottom_0().right_0().h(size)
3581                                    }
3582                                    Some(SplitDirection::Left) => {
3583                                        div.top_0().left_0().bottom_0().w(size)
3584                                    }
3585                                    Some(SplitDirection::Right) => {
3586                                        div.top_0().bottom_0().right_0().w(size)
3587                                    }
3588                                }
3589                            }),
3590                    )
3591            })
3592            .on_mouse_down(
3593                MouseButton::Navigate(NavigationDirection::Back),
3594                cx.listener(|pane, _, window, cx| {
3595                    if let Some(workspace) = pane.workspace.upgrade() {
3596                        let pane = cx.entity().downgrade();
3597                        window.defer(cx, move |window, cx| {
3598                            workspace.update(cx, |workspace, cx| {
3599                                workspace.go_back(pane, window, cx).detach_and_log_err(cx)
3600                            })
3601                        })
3602                    }
3603                }),
3604            )
3605            .on_mouse_down(
3606                MouseButton::Navigate(NavigationDirection::Forward),
3607                cx.listener(|pane, _, window, cx| {
3608                    if let Some(workspace) = pane.workspace.upgrade() {
3609                        let pane = cx.entity().downgrade();
3610                        window.defer(cx, move |window, cx| {
3611                            workspace.update(cx, |workspace, cx| {
3612                                workspace
3613                                    .go_forward(pane, window, cx)
3614                                    .detach_and_log_err(cx)
3615                            })
3616                        })
3617                    }
3618                }),
3619            )
3620    }
3621}
3622
3623impl ItemNavHistory {
3624    pub fn push<D: 'static + Send + Any>(&mut self, data: Option<D>, cx: &mut App) {
3625        if self
3626            .item
3627            .upgrade()
3628            .is_some_and(|item| item.include_in_nav_history())
3629        {
3630            self.history
3631                .push(data, self.item.clone(), self.is_preview, cx);
3632        }
3633    }
3634
3635    pub fn pop_backward(&mut self, cx: &mut App) -> Option<NavigationEntry> {
3636        self.history.pop(NavigationMode::GoingBack, cx)
3637    }
3638
3639    pub fn pop_forward(&mut self, cx: &mut App) -> Option<NavigationEntry> {
3640        self.history.pop(NavigationMode::GoingForward, cx)
3641    }
3642}
3643
3644impl NavHistory {
3645    pub fn for_each_entry(
3646        &self,
3647        cx: &App,
3648        mut f: impl FnMut(&NavigationEntry, (ProjectPath, Option<PathBuf>)),
3649    ) {
3650        let borrowed_history = self.0.lock();
3651        borrowed_history
3652            .forward_stack
3653            .iter()
3654            .chain(borrowed_history.backward_stack.iter())
3655            .chain(borrowed_history.closed_stack.iter())
3656            .for_each(|entry| {
3657                if let Some(project_and_abs_path) =
3658                    borrowed_history.paths_by_item.get(&entry.item.id())
3659                {
3660                    f(entry, project_and_abs_path.clone());
3661                } else if let Some(item) = entry.item.upgrade() {
3662                    if let Some(path) = item.project_path(cx) {
3663                        f(entry, (path, None));
3664                    }
3665                }
3666            })
3667    }
3668
3669    pub fn set_mode(&mut self, mode: NavigationMode) {
3670        self.0.lock().mode = mode;
3671    }
3672
3673    pub fn mode(&self) -> NavigationMode {
3674        self.0.lock().mode
3675    }
3676
3677    pub fn disable(&mut self) {
3678        self.0.lock().mode = NavigationMode::Disabled;
3679    }
3680
3681    pub fn enable(&mut self) {
3682        self.0.lock().mode = NavigationMode::Normal;
3683    }
3684
3685    pub fn pop(&mut self, mode: NavigationMode, cx: &mut App) -> Option<NavigationEntry> {
3686        let mut state = self.0.lock();
3687        let entry = match mode {
3688            NavigationMode::Normal | NavigationMode::Disabled | NavigationMode::ClosingItem => {
3689                return None;
3690            }
3691            NavigationMode::GoingBack => &mut state.backward_stack,
3692            NavigationMode::GoingForward => &mut state.forward_stack,
3693            NavigationMode::ReopeningClosedItem => &mut state.closed_stack,
3694        }
3695        .pop_back();
3696        if entry.is_some() {
3697            state.did_update(cx);
3698        }
3699        entry
3700    }
3701
3702    pub fn push<D: 'static + Send + Any>(
3703        &mut self,
3704        data: Option<D>,
3705        item: Arc<dyn WeakItemHandle>,
3706        is_preview: bool,
3707        cx: &mut App,
3708    ) {
3709        let state = &mut *self.0.lock();
3710        match state.mode {
3711            NavigationMode::Disabled => {}
3712            NavigationMode::Normal | NavigationMode::ReopeningClosedItem => {
3713                if state.backward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
3714                    state.backward_stack.pop_front();
3715                }
3716                state.backward_stack.push_back(NavigationEntry {
3717                    item,
3718                    data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
3719                    timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
3720                    is_preview,
3721                });
3722                state.forward_stack.clear();
3723            }
3724            NavigationMode::GoingBack => {
3725                if state.forward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
3726                    state.forward_stack.pop_front();
3727                }
3728                state.forward_stack.push_back(NavigationEntry {
3729                    item,
3730                    data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
3731                    timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
3732                    is_preview,
3733                });
3734            }
3735            NavigationMode::GoingForward => {
3736                if state.backward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
3737                    state.backward_stack.pop_front();
3738                }
3739                state.backward_stack.push_back(NavigationEntry {
3740                    item,
3741                    data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
3742                    timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
3743                    is_preview,
3744                });
3745            }
3746            NavigationMode::ClosingItem => {
3747                if state.closed_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
3748                    state.closed_stack.pop_front();
3749                }
3750                state.closed_stack.push_back(NavigationEntry {
3751                    item,
3752                    data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
3753                    timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
3754                    is_preview,
3755                });
3756            }
3757        }
3758        state.did_update(cx);
3759    }
3760
3761    pub fn remove_item(&mut self, item_id: EntityId) {
3762        let mut state = self.0.lock();
3763        state.paths_by_item.remove(&item_id);
3764        state
3765            .backward_stack
3766            .retain(|entry| entry.item.id() != item_id);
3767        state
3768            .forward_stack
3769            .retain(|entry| entry.item.id() != item_id);
3770        state
3771            .closed_stack
3772            .retain(|entry| entry.item.id() != item_id);
3773    }
3774
3775    pub fn path_for_item(&self, item_id: EntityId) -> Option<(ProjectPath, Option<PathBuf>)> {
3776        self.0.lock().paths_by_item.get(&item_id).cloned()
3777    }
3778}
3779
3780impl NavHistoryState {
3781    pub fn did_update(&self, cx: &mut App) {
3782        if let Some(pane) = self.pane.upgrade() {
3783            cx.defer(move |cx| {
3784                pane.update(cx, |pane, cx| pane.history_updated(cx));
3785            });
3786        }
3787    }
3788}
3789
3790fn dirty_message_for(buffer_path: Option<ProjectPath>) -> String {
3791    let path = buffer_path
3792        .as_ref()
3793        .and_then(|p| {
3794            p.path
3795                .to_str()
3796                .and_then(|s| if s.is_empty() { None } else { Some(s) })
3797        })
3798        .unwrap_or("This buffer");
3799    let path = truncate_and_remove_front(path, 80);
3800    format!("{path} contains unsaved edits. Do you want to save it?")
3801}
3802
3803pub fn tab_details(items: &[Box<dyn ItemHandle>], _window: &Window, cx: &App) -> Vec<usize> {
3804    let mut tab_details = items.iter().map(|_| 0).collect::<Vec<_>>();
3805    let mut tab_descriptions = HashMap::default();
3806    let mut done = false;
3807    while !done {
3808        done = true;
3809
3810        // Store item indices by their tab description.
3811        for (ix, (item, detail)) in items.iter().zip(&tab_details).enumerate() {
3812            let description = item.tab_content_text(*detail, cx);
3813            if *detail == 0 || description != item.tab_content_text(detail - 1, cx) {
3814                tab_descriptions
3815                    .entry(description)
3816                    .or_insert(Vec::new())
3817                    .push(ix);
3818            }
3819        }
3820
3821        // If two or more items have the same tab description, increase their level
3822        // of detail and try again.
3823        for (_, item_ixs) in tab_descriptions.drain() {
3824            if item_ixs.len() > 1 {
3825                done = false;
3826                for ix in item_ixs {
3827                    tab_details[ix] += 1;
3828                }
3829            }
3830        }
3831    }
3832
3833    tab_details
3834}
3835
3836pub fn render_item_indicator(item: Box<dyn ItemHandle>, cx: &App) -> Option<Indicator> {
3837    maybe!({
3838        let indicator_color = match (item.has_conflict(cx), item.is_dirty(cx)) {
3839            (true, _) => Color::Warning,
3840            (_, true) => Color::Accent,
3841            (false, false) => return None,
3842        };
3843
3844        Some(Indicator::dot().color(indicator_color))
3845    })
3846}
3847
3848impl Render for DraggedTab {
3849    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
3850        let ui_font = ThemeSettings::get_global(cx).ui_font.clone();
3851        let label = self.item.tab_content(
3852            TabContentParams {
3853                detail: Some(self.detail),
3854                selected: false,
3855                preview: false,
3856                deemphasized: false,
3857            },
3858            window,
3859            cx,
3860        );
3861        Tab::new("")
3862            .toggle_state(self.is_active)
3863            .child(label)
3864            .render(window, cx)
3865            .font(ui_font)
3866    }
3867}
3868
3869#[cfg(test)]
3870mod tests {
3871    use std::num::NonZero;
3872
3873    use super::*;
3874    use crate::item::test::{TestItem, TestProjectItem};
3875    use gpui::{TestAppContext, VisualTestContext};
3876    use project::FakeFs;
3877    use settings::SettingsStore;
3878    use theme::LoadThemes;
3879    use util::TryFutureExt;
3880
3881    #[gpui::test]
3882    async fn test_add_item_capped_to_max_tabs(cx: &mut TestAppContext) {
3883        init_test(cx);
3884        let fs = FakeFs::new(cx.executor());
3885
3886        let project = Project::test(fs, None, cx).await;
3887        let (workspace, cx) =
3888            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
3889        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
3890
3891        for i in 0..7 {
3892            add_labeled_item(&pane, format!("{}", i).as_str(), false, cx);
3893        }
3894
3895        set_max_tabs(cx, Some(5));
3896        add_labeled_item(&pane, "7", false, cx);
3897        // Remove items to respect the max tab cap.
3898        assert_item_labels(&pane, ["3", "4", "5", "6", "7*"], cx);
3899        pane.update_in(cx, |pane, window, cx| {
3900            pane.activate_item(0, false, false, window, cx);
3901        });
3902        add_labeled_item(&pane, "X", false, cx);
3903        // Respect activation order.
3904        assert_item_labels(&pane, ["3", "X*", "5", "6", "7"], cx);
3905
3906        for i in 0..7 {
3907            add_labeled_item(&pane, format!("D{}", i).as_str(), true, cx);
3908        }
3909        // Keeps dirty items, even over max tab cap.
3910        assert_item_labels(
3911            &pane,
3912            ["D0^", "D1^", "D2^", "D3^", "D4^", "D5^", "D6*^"],
3913            cx,
3914        );
3915
3916        set_max_tabs(cx, None);
3917        for i in 0..7 {
3918            add_labeled_item(&pane, format!("N{}", i).as_str(), false, cx);
3919        }
3920        // No cap when max tabs is None.
3921        assert_item_labels(
3922            &pane,
3923            [
3924                "D0^", "D1^", "D2^", "D3^", "D4^", "D5^", "D6^", "N0", "N1", "N2", "N3", "N4",
3925                "N5", "N6*",
3926            ],
3927            cx,
3928        );
3929    }
3930
3931    #[gpui::test]
3932    async fn test_reduce_max_tabs_closes_existing_items(cx: &mut TestAppContext) {
3933        init_test(cx);
3934        let fs = FakeFs::new(cx.executor());
3935
3936        let project = Project::test(fs, None, cx).await;
3937        let (workspace, cx) =
3938            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
3939        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
3940
3941        add_labeled_item(&pane, "A", false, cx);
3942        add_labeled_item(&pane, "B", false, cx);
3943        let item_c = add_labeled_item(&pane, "C", false, cx);
3944        let item_d = add_labeled_item(&pane, "D", false, cx);
3945        add_labeled_item(&pane, "E", false, cx);
3946        add_labeled_item(&pane, "Settings", false, cx);
3947        assert_item_labels(&pane, ["A", "B", "C", "D", "E", "Settings*"], cx);
3948
3949        set_max_tabs(cx, Some(5));
3950        assert_item_labels(&pane, ["B", "C", "D", "E", "Settings*"], cx);
3951
3952        set_max_tabs(cx, Some(4));
3953        assert_item_labels(&pane, ["C", "D", "E", "Settings*"], cx);
3954
3955        pane.update_in(cx, |pane, window, cx| {
3956            let ix = pane.index_for_item_id(item_c.item_id()).unwrap();
3957            pane.pin_tab_at(ix, window, cx);
3958
3959            let ix = pane.index_for_item_id(item_d.item_id()).unwrap();
3960            pane.pin_tab_at(ix, window, cx);
3961        });
3962        assert_item_labels(&pane, ["C!", "D!", "E", "Settings*"], cx);
3963
3964        set_max_tabs(cx, Some(2));
3965        assert_item_labels(&pane, ["C!", "D!", "Settings*"], cx);
3966    }
3967
3968    #[gpui::test]
3969    async fn test_allow_pinning_dirty_item_at_max_tabs(cx: &mut TestAppContext) {
3970        init_test(cx);
3971        let fs = FakeFs::new(cx.executor());
3972
3973        let project = Project::test(fs, None, cx).await;
3974        let (workspace, cx) =
3975            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
3976        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
3977
3978        set_max_tabs(cx, Some(1));
3979        let item_a = add_labeled_item(&pane, "A", true, cx);
3980
3981        pane.update_in(cx, |pane, window, cx| {
3982            let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
3983            pane.pin_tab_at(ix, window, cx);
3984        });
3985        assert_item_labels(&pane, ["A*^!"], cx);
3986    }
3987
3988    #[gpui::test]
3989    async fn test_allow_pinning_non_dirty_item_at_max_tabs(cx: &mut TestAppContext) {
3990        init_test(cx);
3991        let fs = FakeFs::new(cx.executor());
3992
3993        let project = Project::test(fs, None, cx).await;
3994        let (workspace, cx) =
3995            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
3996        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
3997
3998        set_max_tabs(cx, Some(1));
3999        let item_a = add_labeled_item(&pane, "A", false, cx);
4000
4001        pane.update_in(cx, |pane, window, cx| {
4002            let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4003            pane.pin_tab_at(ix, window, cx);
4004        });
4005        assert_item_labels(&pane, ["A*!"], cx);
4006    }
4007
4008    #[gpui::test]
4009    async fn test_pin_tabs_incrementally_at_max_capacity(cx: &mut TestAppContext) {
4010        init_test(cx);
4011        let fs = FakeFs::new(cx.executor());
4012
4013        let project = Project::test(fs, None, cx).await;
4014        let (workspace, cx) =
4015            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4016        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4017
4018        set_max_tabs(cx, Some(3));
4019
4020        let item_a = add_labeled_item(&pane, "A", false, cx);
4021        assert_item_labels(&pane, ["A*"], cx);
4022
4023        pane.update_in(cx, |pane, window, cx| {
4024            let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4025            pane.pin_tab_at(ix, window, cx);
4026        });
4027        assert_item_labels(&pane, ["A*!"], cx);
4028
4029        let item_b = add_labeled_item(&pane, "B", false, cx);
4030        assert_item_labels(&pane, ["A!", "B*"], cx);
4031
4032        pane.update_in(cx, |pane, window, cx| {
4033            let ix = pane.index_for_item_id(item_b.item_id()).unwrap();
4034            pane.pin_tab_at(ix, window, cx);
4035        });
4036        assert_item_labels(&pane, ["A!", "B*!"], cx);
4037
4038        let item_c = add_labeled_item(&pane, "C", false, cx);
4039        assert_item_labels(&pane, ["A!", "B!", "C*"], cx);
4040
4041        pane.update_in(cx, |pane, window, cx| {
4042            let ix = pane.index_for_item_id(item_c.item_id()).unwrap();
4043            pane.pin_tab_at(ix, window, cx);
4044        });
4045        assert_item_labels(&pane, ["A!", "B!", "C*!"], cx);
4046    }
4047
4048    #[gpui::test]
4049    async fn test_pin_tabs_left_to_right_after_opening_at_max_capacity(cx: &mut TestAppContext) {
4050        init_test(cx);
4051        let fs = FakeFs::new(cx.executor());
4052
4053        let project = Project::test(fs, None, cx).await;
4054        let (workspace, cx) =
4055            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4056        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4057
4058        set_max_tabs(cx, Some(3));
4059
4060        let item_a = add_labeled_item(&pane, "A", false, cx);
4061        assert_item_labels(&pane, ["A*"], cx);
4062
4063        let item_b = add_labeled_item(&pane, "B", false, cx);
4064        assert_item_labels(&pane, ["A", "B*"], cx);
4065
4066        let item_c = add_labeled_item(&pane, "C", false, cx);
4067        assert_item_labels(&pane, ["A", "B", "C*"], cx);
4068
4069        pane.update_in(cx, |pane, window, cx| {
4070            let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4071            pane.pin_tab_at(ix, window, cx);
4072        });
4073        assert_item_labels(&pane, ["A!", "B", "C*"], cx);
4074
4075        pane.update_in(cx, |pane, window, cx| {
4076            let ix = pane.index_for_item_id(item_b.item_id()).unwrap();
4077            pane.pin_tab_at(ix, window, cx);
4078        });
4079        assert_item_labels(&pane, ["A!", "B!", "C*"], cx);
4080
4081        pane.update_in(cx, |pane, window, cx| {
4082            let ix = pane.index_for_item_id(item_c.item_id()).unwrap();
4083            pane.pin_tab_at(ix, window, cx);
4084        });
4085        assert_item_labels(&pane, ["A!", "B!", "C*!"], cx);
4086    }
4087
4088    #[gpui::test]
4089    async fn test_pin_tabs_right_to_left_after_opening_at_max_capacity(cx: &mut TestAppContext) {
4090        init_test(cx);
4091        let fs = FakeFs::new(cx.executor());
4092
4093        let project = Project::test(fs, None, cx).await;
4094        let (workspace, cx) =
4095            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4096        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4097
4098        set_max_tabs(cx, Some(3));
4099
4100        let item_a = add_labeled_item(&pane, "A", false, cx);
4101        assert_item_labels(&pane, ["A*"], cx);
4102
4103        let item_b = add_labeled_item(&pane, "B", false, cx);
4104        assert_item_labels(&pane, ["A", "B*"], cx);
4105
4106        let item_c = add_labeled_item(&pane, "C", false, cx);
4107        assert_item_labels(&pane, ["A", "B", "C*"], cx);
4108
4109        pane.update_in(cx, |pane, window, cx| {
4110            let ix = pane.index_for_item_id(item_c.item_id()).unwrap();
4111            pane.pin_tab_at(ix, window, cx);
4112        });
4113        assert_item_labels(&pane, ["C*!", "A", "B"], cx);
4114
4115        pane.update_in(cx, |pane, window, cx| {
4116            let ix = pane.index_for_item_id(item_b.item_id()).unwrap();
4117            pane.pin_tab_at(ix, window, cx);
4118        });
4119        assert_item_labels(&pane, ["C*!", "B!", "A"], cx);
4120
4121        pane.update_in(cx, |pane, window, cx| {
4122            let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4123            pane.pin_tab_at(ix, window, cx);
4124        });
4125        assert_item_labels(&pane, ["C*!", "B!", "A!"], cx);
4126    }
4127
4128    #[gpui::test]
4129    async fn test_pinned_tabs_never_closed_at_max_tabs(cx: &mut TestAppContext) {
4130        init_test(cx);
4131        let fs = FakeFs::new(cx.executor());
4132
4133        let project = Project::test(fs, None, cx).await;
4134        let (workspace, cx) =
4135            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4136        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4137
4138        let item_a = add_labeled_item(&pane, "A", false, cx);
4139        pane.update_in(cx, |pane, window, cx| {
4140            let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4141            pane.pin_tab_at(ix, window, cx);
4142        });
4143
4144        let item_b = add_labeled_item(&pane, "B", false, cx);
4145        pane.update_in(cx, |pane, window, cx| {
4146            let ix = pane.index_for_item_id(item_b.item_id()).unwrap();
4147            pane.pin_tab_at(ix, window, cx);
4148        });
4149
4150        add_labeled_item(&pane, "C", false, cx);
4151        add_labeled_item(&pane, "D", false, cx);
4152        add_labeled_item(&pane, "E", false, cx);
4153        assert_item_labels(&pane, ["A!", "B!", "C", "D", "E*"], cx);
4154
4155        set_max_tabs(cx, Some(3));
4156        add_labeled_item(&pane, "F", false, cx);
4157        assert_item_labels(&pane, ["A!", "B!", "F*"], cx);
4158
4159        add_labeled_item(&pane, "G", false, cx);
4160        assert_item_labels(&pane, ["A!", "B!", "G*"], cx);
4161
4162        add_labeled_item(&pane, "H", false, cx);
4163        assert_item_labels(&pane, ["A!", "B!", "H*"], cx);
4164    }
4165
4166    #[gpui::test]
4167    async fn test_always_allows_one_unpinned_item_over_max_tabs_regardless_of_pinned_count(
4168        cx: &mut TestAppContext,
4169    ) {
4170        init_test(cx);
4171        let fs = FakeFs::new(cx.executor());
4172
4173        let project = Project::test(fs, None, cx).await;
4174        let (workspace, cx) =
4175            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4176        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4177
4178        set_max_tabs(cx, Some(3));
4179
4180        let item_a = add_labeled_item(&pane, "A", false, cx);
4181        pane.update_in(cx, |pane, window, cx| {
4182            let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4183            pane.pin_tab_at(ix, window, cx);
4184        });
4185
4186        let item_b = add_labeled_item(&pane, "B", false, cx);
4187        pane.update_in(cx, |pane, window, cx| {
4188            let ix = pane.index_for_item_id(item_b.item_id()).unwrap();
4189            pane.pin_tab_at(ix, window, cx);
4190        });
4191
4192        let item_c = add_labeled_item(&pane, "C", false, cx);
4193        pane.update_in(cx, |pane, window, cx| {
4194            let ix = pane.index_for_item_id(item_c.item_id()).unwrap();
4195            pane.pin_tab_at(ix, window, cx);
4196        });
4197
4198        assert_item_labels(&pane, ["A!", "B!", "C*!"], cx);
4199
4200        let item_d = add_labeled_item(&pane, "D", false, cx);
4201        assert_item_labels(&pane, ["A!", "B!", "C!", "D*"], cx);
4202
4203        pane.update_in(cx, |pane, window, cx| {
4204            let ix = pane.index_for_item_id(item_d.item_id()).unwrap();
4205            pane.pin_tab_at(ix, window, cx);
4206        });
4207        assert_item_labels(&pane, ["A!", "B!", "C!", "D*!"], cx);
4208
4209        add_labeled_item(&pane, "E", false, cx);
4210        assert_item_labels(&pane, ["A!", "B!", "C!", "D!", "E*"], cx);
4211
4212        add_labeled_item(&pane, "F", false, cx);
4213        assert_item_labels(&pane, ["A!", "B!", "C!", "D!", "F*"], cx);
4214    }
4215
4216    #[gpui::test]
4217    async fn test_can_open_one_item_when_all_tabs_are_dirty_at_max(cx: &mut TestAppContext) {
4218        init_test(cx);
4219        let fs = FakeFs::new(cx.executor());
4220
4221        let project = Project::test(fs, None, cx).await;
4222        let (workspace, cx) =
4223            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4224        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4225
4226        set_max_tabs(cx, Some(3));
4227
4228        add_labeled_item(&pane, "A", true, cx);
4229        assert_item_labels(&pane, ["A*^"], cx);
4230
4231        add_labeled_item(&pane, "B", true, cx);
4232        assert_item_labels(&pane, ["A^", "B*^"], cx);
4233
4234        add_labeled_item(&pane, "C", true, cx);
4235        assert_item_labels(&pane, ["A^", "B^", "C*^"], cx);
4236
4237        add_labeled_item(&pane, "D", false, cx);
4238        assert_item_labels(&pane, ["A^", "B^", "C^", "D*"], cx);
4239
4240        add_labeled_item(&pane, "E", false, cx);
4241        assert_item_labels(&pane, ["A^", "B^", "C^", "E*"], cx);
4242
4243        add_labeled_item(&pane, "F", false, cx);
4244        assert_item_labels(&pane, ["A^", "B^", "C^", "F*"], cx);
4245
4246        add_labeled_item(&pane, "G", true, cx);
4247        assert_item_labels(&pane, ["A^", "B^", "C^", "G*^"], cx);
4248    }
4249
4250    #[gpui::test]
4251    async fn test_toggle_pin_tab(cx: &mut TestAppContext) {
4252        init_test(cx);
4253        let fs = FakeFs::new(cx.executor());
4254
4255        let project = Project::test(fs, None, cx).await;
4256        let (workspace, cx) =
4257            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4258        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4259
4260        set_labeled_items(&pane, ["A", "B*", "C"], cx);
4261        assert_item_labels(&pane, ["A", "B*", "C"], cx);
4262
4263        pane.update_in(cx, |pane, window, cx| {
4264            pane.toggle_pin_tab(&TogglePinTab, window, cx);
4265        });
4266        assert_item_labels(&pane, ["B*!", "A", "C"], cx);
4267
4268        pane.update_in(cx, |pane, window, cx| {
4269            pane.toggle_pin_tab(&TogglePinTab, window, cx);
4270        });
4271        assert_item_labels(&pane, ["B*", "A", "C"], cx);
4272    }
4273
4274    #[gpui::test]
4275    async fn test_unpin_all_tabs(cx: &mut TestAppContext) {
4276        init_test(cx);
4277        let fs = FakeFs::new(cx.executor());
4278
4279        let project = Project::test(fs, None, cx).await;
4280        let (workspace, cx) =
4281            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4282        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4283
4284        // Unpin all, in an empty pane
4285        pane.update_in(cx, |pane, window, cx| {
4286            pane.unpin_all_tabs(&UnpinAllTabs, window, cx);
4287        });
4288
4289        assert_item_labels(&pane, [], cx);
4290
4291        let item_a = add_labeled_item(&pane, "A", false, cx);
4292        let item_b = add_labeled_item(&pane, "B", false, cx);
4293        let item_c = add_labeled_item(&pane, "C", false, cx);
4294        assert_item_labels(&pane, ["A", "B", "C*"], cx);
4295
4296        // Unpin all, when no tabs are pinned
4297        pane.update_in(cx, |pane, window, cx| {
4298            pane.unpin_all_tabs(&UnpinAllTabs, window, cx);
4299        });
4300
4301        assert_item_labels(&pane, ["A", "B", "C*"], cx);
4302
4303        // Pin inactive tabs only
4304        pane.update_in(cx, |pane, window, cx| {
4305            let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4306            pane.pin_tab_at(ix, window, cx);
4307
4308            let ix = pane.index_for_item_id(item_b.item_id()).unwrap();
4309            pane.pin_tab_at(ix, window, cx);
4310        });
4311        assert_item_labels(&pane, ["A!", "B!", "C*"], cx);
4312
4313        pane.update_in(cx, |pane, window, cx| {
4314            pane.unpin_all_tabs(&UnpinAllTabs, window, cx);
4315        });
4316
4317        assert_item_labels(&pane, ["A", "B", "C*"], cx);
4318
4319        // Pin all tabs
4320        pane.update_in(cx, |pane, window, cx| {
4321            let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4322            pane.pin_tab_at(ix, window, cx);
4323
4324            let ix = pane.index_for_item_id(item_b.item_id()).unwrap();
4325            pane.pin_tab_at(ix, window, cx);
4326
4327            let ix = pane.index_for_item_id(item_c.item_id()).unwrap();
4328            pane.pin_tab_at(ix, window, cx);
4329        });
4330        assert_item_labels(&pane, ["A!", "B!", "C*!"], cx);
4331
4332        // Activate middle tab
4333        pane.update_in(cx, |pane, window, cx| {
4334            pane.activate_item(1, false, false, window, cx);
4335        });
4336        assert_item_labels(&pane, ["A!", "B*!", "C!"], cx);
4337
4338        pane.update_in(cx, |pane, window, cx| {
4339            pane.unpin_all_tabs(&UnpinAllTabs, window, cx);
4340        });
4341
4342        // Order has not changed
4343        assert_item_labels(&pane, ["A", "B*", "C"], cx);
4344    }
4345
4346    #[gpui::test]
4347    async fn test_pinning_active_tab_without_position_change_maintains_focus(
4348        cx: &mut TestAppContext,
4349    ) {
4350        init_test(cx);
4351        let fs = FakeFs::new(cx.executor());
4352
4353        let project = Project::test(fs, None, cx).await;
4354        let (workspace, cx) =
4355            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4356        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4357
4358        // Add A
4359        let item_a = add_labeled_item(&pane, "A", false, cx);
4360        assert_item_labels(&pane, ["A*"], cx);
4361
4362        // Add B
4363        add_labeled_item(&pane, "B", false, cx);
4364        assert_item_labels(&pane, ["A", "B*"], cx);
4365
4366        // Activate A again
4367        pane.update_in(cx, |pane, window, cx| {
4368            let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4369            pane.activate_item(ix, true, true, window, cx);
4370        });
4371        assert_item_labels(&pane, ["A*", "B"], cx);
4372
4373        // Pin A - remains active
4374        pane.update_in(cx, |pane, window, cx| {
4375            let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4376            pane.pin_tab_at(ix, window, cx);
4377        });
4378        assert_item_labels(&pane, ["A*!", "B"], cx);
4379
4380        // Unpin A - remain active
4381        pane.update_in(cx, |pane, window, cx| {
4382            let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4383            pane.unpin_tab_at(ix, window, cx);
4384        });
4385        assert_item_labels(&pane, ["A*", "B"], cx);
4386    }
4387
4388    #[gpui::test]
4389    async fn test_pinning_active_tab_with_position_change_maintains_focus(cx: &mut TestAppContext) {
4390        init_test(cx);
4391        let fs = FakeFs::new(cx.executor());
4392
4393        let project = Project::test(fs, None, cx).await;
4394        let (workspace, cx) =
4395            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4396        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4397
4398        // Add A, B, C
4399        add_labeled_item(&pane, "A", false, cx);
4400        add_labeled_item(&pane, "B", false, cx);
4401        let item_c = add_labeled_item(&pane, "C", false, cx);
4402        assert_item_labels(&pane, ["A", "B", "C*"], cx);
4403
4404        // Pin C - moves to pinned area, remains active
4405        pane.update_in(cx, |pane, window, cx| {
4406            let ix = pane.index_for_item_id(item_c.item_id()).unwrap();
4407            pane.pin_tab_at(ix, window, cx);
4408        });
4409        assert_item_labels(&pane, ["C*!", "A", "B"], cx);
4410
4411        // Unpin C - moves after pinned area, remains active
4412        pane.update_in(cx, |pane, window, cx| {
4413            let ix = pane.index_for_item_id(item_c.item_id()).unwrap();
4414            pane.unpin_tab_at(ix, window, cx);
4415        });
4416        assert_item_labels(&pane, ["C*", "A", "B"], cx);
4417    }
4418
4419    #[gpui::test]
4420    async fn test_pinning_inactive_tab_without_position_change_preserves_existing_focus(
4421        cx: &mut TestAppContext,
4422    ) {
4423        init_test(cx);
4424        let fs = FakeFs::new(cx.executor());
4425
4426        let project = Project::test(fs, None, cx).await;
4427        let (workspace, cx) =
4428            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4429        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4430
4431        // Add A, B
4432        let item_a = add_labeled_item(&pane, "A", false, cx);
4433        add_labeled_item(&pane, "B", false, cx);
4434        assert_item_labels(&pane, ["A", "B*"], cx);
4435
4436        // Pin A - already in pinned area, B remains active
4437        pane.update_in(cx, |pane, window, cx| {
4438            let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4439            pane.pin_tab_at(ix, window, cx);
4440        });
4441        assert_item_labels(&pane, ["A!", "B*"], cx);
4442
4443        // Unpin A - stays in place, B remains active
4444        pane.update_in(cx, |pane, window, cx| {
4445            let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4446            pane.unpin_tab_at(ix, window, cx);
4447        });
4448        assert_item_labels(&pane, ["A", "B*"], cx);
4449    }
4450
4451    #[gpui::test]
4452    async fn test_pinning_inactive_tab_with_position_change_preserves_existing_focus(
4453        cx: &mut TestAppContext,
4454    ) {
4455        init_test(cx);
4456        let fs = FakeFs::new(cx.executor());
4457
4458        let project = Project::test(fs, None, cx).await;
4459        let (workspace, cx) =
4460            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4461        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4462
4463        // Add A, B, C
4464        add_labeled_item(&pane, "A", false, cx);
4465        let item_b = add_labeled_item(&pane, "B", false, cx);
4466        let item_c = add_labeled_item(&pane, "C", false, cx);
4467        assert_item_labels(&pane, ["A", "B", "C*"], cx);
4468
4469        // Activate B
4470        pane.update_in(cx, |pane, window, cx| {
4471            let ix = pane.index_for_item_id(item_b.item_id()).unwrap();
4472            pane.activate_item(ix, true, true, window, cx);
4473        });
4474        assert_item_labels(&pane, ["A", "B*", "C"], cx);
4475
4476        // Pin C - moves to pinned area, B remains active
4477        pane.update_in(cx, |pane, window, cx| {
4478            let ix = pane.index_for_item_id(item_c.item_id()).unwrap();
4479            pane.pin_tab_at(ix, window, cx);
4480        });
4481        assert_item_labels(&pane, ["C!", "A", "B*"], cx);
4482
4483        // Unpin C - moves after pinned area, B remains active
4484        pane.update_in(cx, |pane, window, cx| {
4485            let ix = pane.index_for_item_id(item_c.item_id()).unwrap();
4486            pane.unpin_tab_at(ix, window, cx);
4487        });
4488        assert_item_labels(&pane, ["C", "A", "B*"], cx);
4489    }
4490
4491    #[gpui::test]
4492    async fn test_drag_unpinned_tab_to_split_creates_pane_with_unpinned_tab(
4493        cx: &mut TestAppContext,
4494    ) {
4495        init_test(cx);
4496        let fs = FakeFs::new(cx.executor());
4497
4498        let project = Project::test(fs, None, cx).await;
4499        let (workspace, cx) =
4500            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4501        let pane_a = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4502
4503        // Add A, B. Pin B. Activate A
4504        let item_a = add_labeled_item(&pane_a, "A", false, cx);
4505        let item_b = add_labeled_item(&pane_a, "B", false, cx);
4506
4507        pane_a.update_in(cx, |pane, window, cx| {
4508            let ix = pane.index_for_item_id(item_b.item_id()).unwrap();
4509            pane.pin_tab_at(ix, window, cx);
4510
4511            let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4512            pane.activate_item(ix, true, true, window, cx);
4513        });
4514
4515        // Drag A to create new split
4516        pane_a.update_in(cx, |pane, window, cx| {
4517            pane.drag_split_direction = Some(SplitDirection::Right);
4518
4519            let dragged_tab = DraggedTab {
4520                pane: pane_a.clone(),
4521                item: item_a.boxed_clone(),
4522                ix: 0,
4523                detail: 0,
4524                is_active: true,
4525            };
4526            pane.handle_tab_drop(&dragged_tab, 0, window, cx);
4527        });
4528
4529        // A should be moved to new pane. B should remain pinned, A should not be pinned
4530        let (pane_a, pane_b) = workspace.read_with(cx, |workspace, _| {
4531            let panes = workspace.panes();
4532            (panes[0].clone(), panes[1].clone())
4533        });
4534        assert_item_labels(&pane_a, ["B*!"], cx);
4535        assert_item_labels(&pane_b, ["A*"], cx);
4536    }
4537
4538    #[gpui::test]
4539    async fn test_drag_pinned_tab_to_split_creates_pane_with_pinned_tab(cx: &mut TestAppContext) {
4540        init_test(cx);
4541        let fs = FakeFs::new(cx.executor());
4542
4543        let project = Project::test(fs, None, cx).await;
4544        let (workspace, cx) =
4545            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4546        let pane_a = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4547
4548        // Add A, B. Pin both. Activate A
4549        let item_a = add_labeled_item(&pane_a, "A", false, cx);
4550        let item_b = add_labeled_item(&pane_a, "B", false, cx);
4551
4552        pane_a.update_in(cx, |pane, window, cx| {
4553            let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4554            pane.pin_tab_at(ix, window, cx);
4555
4556            let ix = pane.index_for_item_id(item_b.item_id()).unwrap();
4557            pane.pin_tab_at(ix, window, cx);
4558
4559            let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4560            pane.activate_item(ix, true, true, window, cx);
4561        });
4562        assert_item_labels(&pane_a, ["A*!", "B!"], cx);
4563
4564        // Drag A to create new split
4565        pane_a.update_in(cx, |pane, window, cx| {
4566            pane.drag_split_direction = Some(SplitDirection::Right);
4567
4568            let dragged_tab = DraggedTab {
4569                pane: pane_a.clone(),
4570                item: item_a.boxed_clone(),
4571                ix: 0,
4572                detail: 0,
4573                is_active: true,
4574            };
4575            pane.handle_tab_drop(&dragged_tab, 0, window, cx);
4576        });
4577
4578        // A should be moved to new pane. Both A and B should still be pinned
4579        let (pane_a, pane_b) = workspace.read_with(cx, |workspace, _| {
4580            let panes = workspace.panes();
4581            (panes[0].clone(), panes[1].clone())
4582        });
4583        assert_item_labels(&pane_a, ["B*!"], cx);
4584        assert_item_labels(&pane_b, ["A*!"], cx);
4585    }
4586
4587    #[gpui::test]
4588    async fn test_drag_pinned_tab_into_existing_panes_pinned_region(cx: &mut TestAppContext) {
4589        init_test(cx);
4590        let fs = FakeFs::new(cx.executor());
4591
4592        let project = Project::test(fs, None, cx).await;
4593        let (workspace, cx) =
4594            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4595        let pane_a = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4596
4597        // Add A to pane A and pin
4598        let item_a = add_labeled_item(&pane_a, "A", false, cx);
4599        pane_a.update_in(cx, |pane, window, cx| {
4600            let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4601            pane.pin_tab_at(ix, window, cx);
4602        });
4603        assert_item_labels(&pane_a, ["A*!"], cx);
4604
4605        // Add B to pane B and pin
4606        let pane_b = workspace.update_in(cx, |workspace, window, cx| {
4607            workspace.split_pane(pane_a.clone(), SplitDirection::Right, window, cx)
4608        });
4609        let item_b = add_labeled_item(&pane_b, "B", false, cx);
4610        pane_b.update_in(cx, |pane, window, cx| {
4611            let ix = pane.index_for_item_id(item_b.item_id()).unwrap();
4612            pane.pin_tab_at(ix, window, cx);
4613        });
4614        assert_item_labels(&pane_b, ["B*!"], cx);
4615
4616        // Move A from pane A to pane B's pinned region
4617        pane_b.update_in(cx, |pane, window, cx| {
4618            let dragged_tab = DraggedTab {
4619                pane: pane_a.clone(),
4620                item: item_a.boxed_clone(),
4621                ix: 0,
4622                detail: 0,
4623                is_active: true,
4624            };
4625            pane.handle_tab_drop(&dragged_tab, 0, window, cx);
4626        });
4627
4628        // A should stay pinned
4629        assert_item_labels(&pane_a, [], cx);
4630        assert_item_labels(&pane_b, ["A*!", "B!"], cx);
4631    }
4632
4633    #[gpui::test]
4634    async fn test_drag_pinned_tab_into_existing_panes_unpinned_region(cx: &mut TestAppContext) {
4635        init_test(cx);
4636        let fs = FakeFs::new(cx.executor());
4637
4638        let project = Project::test(fs, None, cx).await;
4639        let (workspace, cx) =
4640            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4641        let pane_a = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4642
4643        // Add A to pane A and pin
4644        let item_a = add_labeled_item(&pane_a, "A", false, cx);
4645        pane_a.update_in(cx, |pane, window, cx| {
4646            let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4647            pane.pin_tab_at(ix, window, cx);
4648        });
4649        assert_item_labels(&pane_a, ["A*!"], cx);
4650
4651        // Create pane B with pinned item B
4652        let pane_b = workspace.update_in(cx, |workspace, window, cx| {
4653            workspace.split_pane(pane_a.clone(), SplitDirection::Right, window, cx)
4654        });
4655        let item_b = add_labeled_item(&pane_b, "B", false, cx);
4656        assert_item_labels(&pane_b, ["B*"], cx);
4657
4658        pane_b.update_in(cx, |pane, window, cx| {
4659            let ix = pane.index_for_item_id(item_b.item_id()).unwrap();
4660            pane.pin_tab_at(ix, window, cx);
4661        });
4662        assert_item_labels(&pane_b, ["B*!"], cx);
4663
4664        // Move A from pane A to pane B's unpinned region
4665        pane_b.update_in(cx, |pane, window, cx| {
4666            let dragged_tab = DraggedTab {
4667                pane: pane_a.clone(),
4668                item: item_a.boxed_clone(),
4669                ix: 0,
4670                detail: 0,
4671                is_active: true,
4672            };
4673            pane.handle_tab_drop(&dragged_tab, 1, window, cx);
4674        });
4675
4676        // A should become pinned
4677        assert_item_labels(&pane_a, [], cx);
4678        assert_item_labels(&pane_b, ["B!", "A*"], cx);
4679    }
4680
4681    #[gpui::test]
4682    async fn test_drag_pinned_tab_into_existing_panes_first_position_with_no_pinned_tabs(
4683        cx: &mut TestAppContext,
4684    ) {
4685        init_test(cx);
4686        let fs = FakeFs::new(cx.executor());
4687
4688        let project = Project::test(fs, None, cx).await;
4689        let (workspace, cx) =
4690            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4691        let pane_a = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4692
4693        // Add A to pane A and pin
4694        let item_a = add_labeled_item(&pane_a, "A", false, cx);
4695        pane_a.update_in(cx, |pane, window, cx| {
4696            let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4697            pane.pin_tab_at(ix, window, cx);
4698        });
4699        assert_item_labels(&pane_a, ["A*!"], cx);
4700
4701        // Add B to pane B
4702        let pane_b = workspace.update_in(cx, |workspace, window, cx| {
4703            workspace.split_pane(pane_a.clone(), SplitDirection::Right, window, cx)
4704        });
4705        add_labeled_item(&pane_b, "B", false, cx);
4706        assert_item_labels(&pane_b, ["B*"], cx);
4707
4708        // Move A from pane A to position 0 in pane B, indicating it should stay pinned
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, 0, window, cx);
4718        });
4719
4720        // A should stay pinned
4721        assert_item_labels(&pane_a, [], cx);
4722        assert_item_labels(&pane_b, ["A*!", "B"], cx);
4723    }
4724
4725    #[gpui::test]
4726    async fn test_drag_pinned_tab_into_existing_pane_at_max_capacity_closes_unpinned_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        set_max_tabs(cx, Some(2));
4737
4738        // Add A, B to pane A. Pin both
4739        let item_a = add_labeled_item(&pane_a, "A", false, cx);
4740        let item_b = add_labeled_item(&pane_a, "B", false, cx);
4741        pane_a.update_in(cx, |pane, window, cx| {
4742            let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4743            pane.pin_tab_at(ix, window, cx);
4744
4745            let ix = pane.index_for_item_id(item_b.item_id()).unwrap();
4746            pane.pin_tab_at(ix, window, cx);
4747        });
4748        assert_item_labels(&pane_a, ["A!", "B*!"], cx);
4749
4750        // Add C, D to pane B. Pin both
4751        let pane_b = workspace.update_in(cx, |workspace, window, cx| {
4752            workspace.split_pane(pane_a.clone(), SplitDirection::Right, window, cx)
4753        });
4754        let item_c = add_labeled_item(&pane_b, "C", false, cx);
4755        let item_d = add_labeled_item(&pane_b, "D", false, cx);
4756        pane_b.update_in(cx, |pane, window, cx| {
4757            let ix = pane.index_for_item_id(item_c.item_id()).unwrap();
4758            pane.pin_tab_at(ix, window, cx);
4759
4760            let ix = pane.index_for_item_id(item_d.item_id()).unwrap();
4761            pane.pin_tab_at(ix, window, cx);
4762        });
4763        assert_item_labels(&pane_b, ["C!", "D*!"], cx);
4764
4765        // Add a third unpinned item to pane B (exceeds max tabs), but is allowed,
4766        // as we allow 1 tab over max if the others are pinned or dirty
4767        add_labeled_item(&pane_b, "E", false, cx);
4768        assert_item_labels(&pane_b, ["C!", "D!", "E*"], cx);
4769
4770        // Drag pinned A from pane A to position 0 in pane B
4771        pane_b.update_in(cx, |pane, window, cx| {
4772            let dragged_tab = DraggedTab {
4773                pane: pane_a.clone(),
4774                item: item_a.boxed_clone(),
4775                ix: 0,
4776                detail: 0,
4777                is_active: true,
4778            };
4779            pane.handle_tab_drop(&dragged_tab, 0, window, cx);
4780        });
4781
4782        // E (unpinned) should be closed, leaving 3 pinned items
4783        assert_item_labels(&pane_a, ["B*!"], cx);
4784        assert_item_labels(&pane_b, ["A*!", "C!", "D!"], cx);
4785    }
4786
4787    #[gpui::test]
4788    async fn test_drag_last_pinned_tab_to_same_position_stays_pinned(cx: &mut TestAppContext) {
4789        init_test(cx);
4790        let fs = FakeFs::new(cx.executor());
4791
4792        let project = Project::test(fs, None, cx).await;
4793        let (workspace, cx) =
4794            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4795        let pane_a = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4796
4797        // Add A to pane A and pin it
4798        let item_a = add_labeled_item(&pane_a, "A", false, cx);
4799        pane_a.update_in(cx, |pane, window, cx| {
4800            let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4801            pane.pin_tab_at(ix, window, cx);
4802        });
4803        assert_item_labels(&pane_a, ["A*!"], cx);
4804
4805        // Drag pinned A to position 1 (directly to the right) in the same pane
4806        pane_a.update_in(cx, |pane, window, cx| {
4807            let dragged_tab = DraggedTab {
4808                pane: pane_a.clone(),
4809                item: item_a.boxed_clone(),
4810                ix: 0,
4811                detail: 0,
4812                is_active: true,
4813            };
4814            pane.handle_tab_drop(&dragged_tab, 1, window, cx);
4815        });
4816
4817        // A should still be pinned and active
4818        assert_item_labels(&pane_a, ["A*!"], cx);
4819    }
4820
4821    #[gpui::test]
4822    async fn test_drag_pinned_tab_beyond_last_pinned_tab_in_same_pane_stays_pinned(
4823        cx: &mut TestAppContext,
4824    ) {
4825        init_test(cx);
4826        let fs = FakeFs::new(cx.executor());
4827
4828        let project = Project::test(fs, None, cx).await;
4829        let (workspace, cx) =
4830            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4831        let pane_a = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4832
4833        // Add A, B to pane A and pin both
4834        let item_a = add_labeled_item(&pane_a, "A", false, cx);
4835        let item_b = add_labeled_item(&pane_a, "B", false, cx);
4836        pane_a.update_in(cx, |pane, window, cx| {
4837            let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4838            pane.pin_tab_at(ix, window, cx);
4839
4840            let ix = pane.index_for_item_id(item_b.item_id()).unwrap();
4841            pane.pin_tab_at(ix, window, cx);
4842        });
4843        assert_item_labels(&pane_a, ["A!", "B*!"], cx);
4844
4845        // Drag pinned A right of B in the same pane
4846        pane_a.update_in(cx, |pane, window, cx| {
4847            let dragged_tab = DraggedTab {
4848                pane: pane_a.clone(),
4849                item: item_a.boxed_clone(),
4850                ix: 0,
4851                detail: 0,
4852                is_active: true,
4853            };
4854            pane.handle_tab_drop(&dragged_tab, 2, window, cx);
4855        });
4856
4857        // A stays pinned
4858        assert_item_labels(&pane_a, ["B!", "A*!"], cx);
4859    }
4860
4861    #[gpui::test]
4862    async fn test_drag_pinned_tab_beyond_unpinned_tab_in_same_pane_becomes_unpinned(
4863        cx: &mut TestAppContext,
4864    ) {
4865        init_test(cx);
4866        let fs = FakeFs::new(cx.executor());
4867
4868        let project = Project::test(fs, None, cx).await;
4869        let (workspace, cx) =
4870            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4871        let pane_a = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4872
4873        // Add A, B to pane A and pin A
4874        let item_a = add_labeled_item(&pane_a, "A", false, cx);
4875        add_labeled_item(&pane_a, "B", false, cx);
4876        pane_a.update_in(cx, |pane, window, cx| {
4877            let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4878            pane.pin_tab_at(ix, window, cx);
4879        });
4880        assert_item_labels(&pane_a, ["A!", "B*"], cx);
4881
4882        // Drag pinned A right of B in the same pane
4883        pane_a.update_in(cx, |pane, window, cx| {
4884            let dragged_tab = DraggedTab {
4885                pane: pane_a.clone(),
4886                item: item_a.boxed_clone(),
4887                ix: 0,
4888                detail: 0,
4889                is_active: true,
4890            };
4891            pane.handle_tab_drop(&dragged_tab, 2, window, cx);
4892        });
4893
4894        // A becomes unpinned
4895        assert_item_labels(&pane_a, ["B", "A*"], cx);
4896    }
4897
4898    #[gpui::test]
4899    async fn test_drag_unpinned_tab_in_front_of_pinned_tab_in_same_pane_becomes_pinned(
4900        cx: &mut TestAppContext,
4901    ) {
4902        init_test(cx);
4903        let fs = FakeFs::new(cx.executor());
4904
4905        let project = Project::test(fs, None, cx).await;
4906        let (workspace, cx) =
4907            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4908        let pane_a = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4909
4910        // Add A, B to pane A and pin A
4911        let item_a = add_labeled_item(&pane_a, "A", false, cx);
4912        let item_b = add_labeled_item(&pane_a, "B", false, cx);
4913        pane_a.update_in(cx, |pane, window, cx| {
4914            let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4915            pane.pin_tab_at(ix, window, cx);
4916        });
4917        assert_item_labels(&pane_a, ["A!", "B*"], cx);
4918
4919        // Drag pinned B left of A in the same pane
4920        pane_a.update_in(cx, |pane, window, cx| {
4921            let dragged_tab = DraggedTab {
4922                pane: pane_a.clone(),
4923                item: item_b.boxed_clone(),
4924                ix: 1,
4925                detail: 0,
4926                is_active: true,
4927            };
4928            pane.handle_tab_drop(&dragged_tab, 0, window, cx);
4929        });
4930
4931        // A becomes unpinned
4932        assert_item_labels(&pane_a, ["B*!", "A!"], cx);
4933    }
4934
4935    #[gpui::test]
4936    async fn test_drag_unpinned_tab_to_the_pinned_region_stays_pinned(cx: &mut TestAppContext) {
4937        init_test(cx);
4938        let fs = FakeFs::new(cx.executor());
4939
4940        let project = Project::test(fs, None, cx).await;
4941        let (workspace, cx) =
4942            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4943        let pane_a = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4944
4945        // Add A, B, C to pane A and pin A
4946        let item_a = add_labeled_item(&pane_a, "A", false, cx);
4947        add_labeled_item(&pane_a, "B", false, cx);
4948        let item_c = add_labeled_item(&pane_a, "C", false, cx);
4949        pane_a.update_in(cx, |pane, window, cx| {
4950            let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4951            pane.pin_tab_at(ix, window, cx);
4952        });
4953        assert_item_labels(&pane_a, ["A!", "B", "C*"], cx);
4954
4955        // Drag pinned C left of B in the same pane
4956        pane_a.update_in(cx, |pane, window, cx| {
4957            let dragged_tab = DraggedTab {
4958                pane: pane_a.clone(),
4959                item: item_c.boxed_clone(),
4960                ix: 2,
4961                detail: 0,
4962                is_active: true,
4963            };
4964            pane.handle_tab_drop(&dragged_tab, 1, window, cx);
4965        });
4966
4967        // A stays pinned, B and C remain unpinned
4968        assert_item_labels(&pane_a, ["A!", "C*", "B"], cx);
4969    }
4970
4971    #[gpui::test]
4972    async fn test_drag_unpinned_tab_into_existing_panes_pinned_region(cx: &mut TestAppContext) {
4973        init_test(cx);
4974        let fs = FakeFs::new(cx.executor());
4975
4976        let project = Project::test(fs, None, cx).await;
4977        let (workspace, cx) =
4978            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4979        let pane_a = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4980
4981        // Add unpinned item A to pane A
4982        let item_a = add_labeled_item(&pane_a, "A", false, cx);
4983        assert_item_labels(&pane_a, ["A*"], cx);
4984
4985        // Create pane B with pinned item B
4986        let pane_b = workspace.update_in(cx, |workspace, window, cx| {
4987            workspace.split_pane(pane_a.clone(), SplitDirection::Right, window, cx)
4988        });
4989        let item_b = add_labeled_item(&pane_b, "B", false, cx);
4990        pane_b.update_in(cx, |pane, window, cx| {
4991            let ix = pane.index_for_item_id(item_b.item_id()).unwrap();
4992            pane.pin_tab_at(ix, window, cx);
4993        });
4994        assert_item_labels(&pane_b, ["B*!"], cx);
4995
4996        // Move A from pane A to pane B's pinned region
4997        pane_b.update_in(cx, |pane, window, cx| {
4998            let dragged_tab = DraggedTab {
4999                pane: pane_a.clone(),
5000                item: item_a.boxed_clone(),
5001                ix: 0,
5002                detail: 0,
5003                is_active: true,
5004            };
5005            pane.handle_tab_drop(&dragged_tab, 0, window, cx);
5006        });
5007
5008        // A should become pinned since it was dropped in the pinned region
5009        assert_item_labels(&pane_a, [], cx);
5010        assert_item_labels(&pane_b, ["A*!", "B!"], cx);
5011    }
5012
5013    #[gpui::test]
5014    async fn test_drag_unpinned_tab_into_existing_panes_unpinned_region(cx: &mut TestAppContext) {
5015        init_test(cx);
5016        let fs = FakeFs::new(cx.executor());
5017
5018        let project = Project::test(fs, None, cx).await;
5019        let (workspace, cx) =
5020            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
5021        let pane_a = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
5022
5023        // Add unpinned item A to pane A
5024        let item_a = add_labeled_item(&pane_a, "A", false, cx);
5025        assert_item_labels(&pane_a, ["A*"], cx);
5026
5027        // Create pane B with one pinned item B
5028        let pane_b = workspace.update_in(cx, |workspace, window, cx| {
5029            workspace.split_pane(pane_a.clone(), SplitDirection::Right, window, cx)
5030        });
5031        let item_b = add_labeled_item(&pane_b, "B", false, cx);
5032        pane_b.update_in(cx, |pane, window, cx| {
5033            let ix = pane.index_for_item_id(item_b.item_id()).unwrap();
5034            pane.pin_tab_at(ix, window, cx);
5035        });
5036        assert_item_labels(&pane_b, ["B*!"], cx);
5037
5038        // Move A from pane A to pane B's unpinned region
5039        pane_b.update_in(cx, |pane, window, cx| {
5040            let dragged_tab = DraggedTab {
5041                pane: pane_a.clone(),
5042                item: item_a.boxed_clone(),
5043                ix: 0,
5044                detail: 0,
5045                is_active: true,
5046            };
5047            pane.handle_tab_drop(&dragged_tab, 1, window, cx);
5048        });
5049
5050        // A should remain unpinned since it was dropped outside the pinned region
5051        assert_item_labels(&pane_a, [], cx);
5052        assert_item_labels(&pane_b, ["B!", "A*"], cx);
5053    }
5054
5055    #[gpui::test]
5056    async fn test_drag_pinned_tab_throughout_entire_range_of_pinned_tabs_both_directions(
5057        cx: &mut TestAppContext,
5058    ) {
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 A, B, C and pin all
5068        let item_a = add_labeled_item(&pane_a, "A", false, cx);
5069        let item_b = add_labeled_item(&pane_a, "B", false, cx);
5070        let item_c = add_labeled_item(&pane_a, "C", false, cx);
5071        assert_item_labels(&pane_a, ["A", "B", "C*"], cx);
5072
5073        pane_a.update_in(cx, |pane, window, cx| {
5074            let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
5075            pane.pin_tab_at(ix, window, cx);
5076
5077            let ix = pane.index_for_item_id(item_b.item_id()).unwrap();
5078            pane.pin_tab_at(ix, window, cx);
5079
5080            let ix = pane.index_for_item_id(item_c.item_id()).unwrap();
5081            pane.pin_tab_at(ix, window, cx);
5082        });
5083        assert_item_labels(&pane_a, ["A!", "B!", "C*!"], cx);
5084
5085        // Move A to right of B
5086        pane_a.update_in(cx, |pane, window, cx| {
5087            let dragged_tab = DraggedTab {
5088                pane: pane_a.clone(),
5089                item: item_a.boxed_clone(),
5090                ix: 0,
5091                detail: 0,
5092                is_active: true,
5093            };
5094            pane.handle_tab_drop(&dragged_tab, 1, window, cx);
5095        });
5096
5097        // A should be after B and all are pinned
5098        assert_item_labels(&pane_a, ["B!", "A*!", "C!"], cx);
5099
5100        // Move A to right of C
5101        pane_a.update_in(cx, |pane, window, cx| {
5102            let dragged_tab = DraggedTab {
5103                pane: pane_a.clone(),
5104                item: item_a.boxed_clone(),
5105                ix: 1,
5106                detail: 0,
5107                is_active: true,
5108            };
5109            pane.handle_tab_drop(&dragged_tab, 2, window, cx);
5110        });
5111
5112        // A should be after C and all are pinned
5113        assert_item_labels(&pane_a, ["B!", "C!", "A*!"], cx);
5114
5115        // Move A to left of C
5116        pane_a.update_in(cx, |pane, window, cx| {
5117            let dragged_tab = DraggedTab {
5118                pane: pane_a.clone(),
5119                item: item_a.boxed_clone(),
5120                ix: 2,
5121                detail: 0,
5122                is_active: true,
5123            };
5124            pane.handle_tab_drop(&dragged_tab, 1, window, cx);
5125        });
5126
5127        // A should be before C and all are pinned
5128        assert_item_labels(&pane_a, ["B!", "A*!", "C!"], cx);
5129
5130        // Move A to left of B
5131        pane_a.update_in(cx, |pane, window, cx| {
5132            let dragged_tab = DraggedTab {
5133                pane: pane_a.clone(),
5134                item: item_a.boxed_clone(),
5135                ix: 1,
5136                detail: 0,
5137                is_active: true,
5138            };
5139            pane.handle_tab_drop(&dragged_tab, 0, window, cx);
5140        });
5141
5142        // A should be before B and all are pinned
5143        assert_item_labels(&pane_a, ["A*!", "B!", "C!"], cx);
5144    }
5145
5146    #[gpui::test]
5147    async fn test_drag_first_tab_to_last_position(cx: &mut TestAppContext) {
5148        init_test(cx);
5149        let fs = FakeFs::new(cx.executor());
5150
5151        let project = Project::test(fs, None, cx).await;
5152        let (workspace, cx) =
5153            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
5154        let pane_a = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
5155
5156        // Add A, B, C
5157        let item_a = add_labeled_item(&pane_a, "A", false, cx);
5158        add_labeled_item(&pane_a, "B", false, cx);
5159        add_labeled_item(&pane_a, "C", false, cx);
5160        assert_item_labels(&pane_a, ["A", "B", "C*"], cx);
5161
5162        // Move A to the end
5163        pane_a.update_in(cx, |pane, window, cx| {
5164            let dragged_tab = DraggedTab {
5165                pane: pane_a.clone(),
5166                item: item_a.boxed_clone(),
5167                ix: 0,
5168                detail: 0,
5169                is_active: true,
5170            };
5171            pane.handle_tab_drop(&dragged_tab, 2, window, cx);
5172        });
5173
5174        // A should be at the end
5175        assert_item_labels(&pane_a, ["B", "C", "A*"], cx);
5176    }
5177
5178    #[gpui::test]
5179    async fn test_drag_last_tab_to_first_position(cx: &mut TestAppContext) {
5180        init_test(cx);
5181        let fs = FakeFs::new(cx.executor());
5182
5183        let project = Project::test(fs, None, cx).await;
5184        let (workspace, cx) =
5185            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
5186        let pane_a = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
5187
5188        // Add A, B, C
5189        add_labeled_item(&pane_a, "A", false, cx);
5190        add_labeled_item(&pane_a, "B", false, cx);
5191        let item_c = add_labeled_item(&pane_a, "C", false, cx);
5192        assert_item_labels(&pane_a, ["A", "B", "C*"], cx);
5193
5194        // Move C to the beginning
5195        pane_a.update_in(cx, |pane, window, cx| {
5196            let dragged_tab = DraggedTab {
5197                pane: pane_a.clone(),
5198                item: item_c.boxed_clone(),
5199                ix: 2,
5200                detail: 0,
5201                is_active: true,
5202            };
5203            pane.handle_tab_drop(&dragged_tab, 0, window, cx);
5204        });
5205
5206        // C should be at the beginning
5207        assert_item_labels(&pane_a, ["C*", "A", "B"], cx);
5208    }
5209
5210    #[gpui::test]
5211    async fn test_add_item_with_new_item(cx: &mut TestAppContext) {
5212        init_test(cx);
5213        let fs = FakeFs::new(cx.executor());
5214
5215        let project = Project::test(fs, None, cx).await;
5216        let (workspace, cx) =
5217            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
5218        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
5219
5220        // 1. Add with a destination index
5221        //   a. Add before the active item
5222        set_labeled_items(&pane, ["A", "B*", "C"], cx);
5223        pane.update_in(cx, |pane, window, cx| {
5224            pane.add_item(
5225                Box::new(cx.new(|cx| TestItem::new(cx).with_label("D"))),
5226                false,
5227                false,
5228                Some(0),
5229                window,
5230                cx,
5231            );
5232        });
5233        assert_item_labels(&pane, ["D*", "A", "B", "C"], cx);
5234
5235        //   b. Add after the active item
5236        set_labeled_items(&pane, ["A", "B*", "C"], cx);
5237        pane.update_in(cx, |pane, window, cx| {
5238            pane.add_item(
5239                Box::new(cx.new(|cx| TestItem::new(cx).with_label("D"))),
5240                false,
5241                false,
5242                Some(2),
5243                window,
5244                cx,
5245            );
5246        });
5247        assert_item_labels(&pane, ["A", "B", "D*", "C"], cx);
5248
5249        //   c. Add at the end of the item list (including off the length)
5250        set_labeled_items(&pane, ["A", "B*", "C"], cx);
5251        pane.update_in(cx, |pane, window, cx| {
5252            pane.add_item(
5253                Box::new(cx.new(|cx| TestItem::new(cx).with_label("D"))),
5254                false,
5255                false,
5256                Some(5),
5257                window,
5258                cx,
5259            );
5260        });
5261        assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
5262
5263        // 2. Add without a destination index
5264        //   a. Add with active item at the start of the item list
5265        set_labeled_items(&pane, ["A*", "B", "C"], cx);
5266        pane.update_in(cx, |pane, window, cx| {
5267            pane.add_item(
5268                Box::new(cx.new(|cx| TestItem::new(cx).with_label("D"))),
5269                false,
5270                false,
5271                None,
5272                window,
5273                cx,
5274            );
5275        });
5276        set_labeled_items(&pane, ["A", "D*", "B", "C"], cx);
5277
5278        //   b. Add with active item at the end of the item list
5279        set_labeled_items(&pane, ["A", "B", "C*"], cx);
5280        pane.update_in(cx, |pane, window, cx| {
5281            pane.add_item(
5282                Box::new(cx.new(|cx| TestItem::new(cx).with_label("D"))),
5283                false,
5284                false,
5285                None,
5286                window,
5287                cx,
5288            );
5289        });
5290        assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
5291    }
5292
5293    #[gpui::test]
5294    async fn test_add_item_with_existing_item(cx: &mut TestAppContext) {
5295        init_test(cx);
5296        let fs = FakeFs::new(cx.executor());
5297
5298        let project = Project::test(fs, None, cx).await;
5299        let (workspace, cx) =
5300            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
5301        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
5302
5303        // 1. Add with a destination index
5304        //   1a. Add before the active item
5305        let [_, _, _, d] = set_labeled_items(&pane, ["A", "B*", "C", "D"], cx);
5306        pane.update_in(cx, |pane, window, cx| {
5307            pane.add_item(d, false, false, Some(0), window, cx);
5308        });
5309        assert_item_labels(&pane, ["D*", "A", "B", "C"], cx);
5310
5311        //   1b. Add after the active item
5312        let [_, _, _, d] = set_labeled_items(&pane, ["A", "B*", "C", "D"], cx);
5313        pane.update_in(cx, |pane, window, cx| {
5314            pane.add_item(d, false, false, Some(2), window, cx);
5315        });
5316        assert_item_labels(&pane, ["A", "B", "D*", "C"], cx);
5317
5318        //   1c. Add at the end of the item list (including off the length)
5319        let [a, _, _, _] = set_labeled_items(&pane, ["A", "B*", "C", "D"], cx);
5320        pane.update_in(cx, |pane, window, cx| {
5321            pane.add_item(a, false, false, Some(5), window, cx);
5322        });
5323        assert_item_labels(&pane, ["B", "C", "D", "A*"], cx);
5324
5325        //   1d. Add same item to active index
5326        let [_, b, _] = set_labeled_items(&pane, ["A", "B*", "C"], cx);
5327        pane.update_in(cx, |pane, window, cx| {
5328            pane.add_item(b, false, false, Some(1), window, cx);
5329        });
5330        assert_item_labels(&pane, ["A", "B*", "C"], cx);
5331
5332        //   1e. Add item to index after same item in last position
5333        let [_, _, c] = set_labeled_items(&pane, ["A", "B*", "C"], cx);
5334        pane.update_in(cx, |pane, window, cx| {
5335            pane.add_item(c, false, false, Some(2), window, cx);
5336        });
5337        assert_item_labels(&pane, ["A", "B", "C*"], cx);
5338
5339        // 2. Add without a destination index
5340        //   2a. Add with active item at the start of the item list
5341        let [_, _, _, d] = set_labeled_items(&pane, ["A*", "B", "C", "D"], cx);
5342        pane.update_in(cx, |pane, window, cx| {
5343            pane.add_item(d, false, false, None, window, cx);
5344        });
5345        assert_item_labels(&pane, ["A", "D*", "B", "C"], cx);
5346
5347        //   2b. Add with active item at the end of the item list
5348        let [a, _, _, _] = set_labeled_items(&pane, ["A", "B", "C", "D*"], cx);
5349        pane.update_in(cx, |pane, window, cx| {
5350            pane.add_item(a, false, false, None, window, cx);
5351        });
5352        assert_item_labels(&pane, ["B", "C", "D", "A*"], cx);
5353
5354        //   2c. Add active item to active item at end of list
5355        let [_, _, c] = set_labeled_items(&pane, ["A", "B", "C*"], cx);
5356        pane.update_in(cx, |pane, window, cx| {
5357            pane.add_item(c, false, false, None, window, cx);
5358        });
5359        assert_item_labels(&pane, ["A", "B", "C*"], cx);
5360
5361        //   2d. Add active item to active item at start of list
5362        let [a, _, _] = set_labeled_items(&pane, ["A*", "B", "C"], cx);
5363        pane.update_in(cx, |pane, window, cx| {
5364            pane.add_item(a, false, false, None, window, cx);
5365        });
5366        assert_item_labels(&pane, ["A*", "B", "C"], cx);
5367    }
5368
5369    #[gpui::test]
5370    async fn test_add_item_with_same_project_entries(cx: &mut TestAppContext) {
5371        init_test(cx);
5372        let fs = FakeFs::new(cx.executor());
5373
5374        let project = Project::test(fs, None, cx).await;
5375        let (workspace, cx) =
5376            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
5377        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
5378
5379        // singleton view
5380        pane.update_in(cx, |pane, window, cx| {
5381            pane.add_item(
5382                Box::new(cx.new(|cx| {
5383                    TestItem::new(cx)
5384                        .with_singleton(true)
5385                        .with_label("buffer 1")
5386                        .with_project_items(&[TestProjectItem::new(1, "one.txt", cx)])
5387                })),
5388                false,
5389                false,
5390                None,
5391                window,
5392                cx,
5393            );
5394        });
5395        assert_item_labels(&pane, ["buffer 1*"], cx);
5396
5397        // new singleton view with the same project entry
5398        pane.update_in(cx, |pane, window, cx| {
5399            pane.add_item(
5400                Box::new(cx.new(|cx| {
5401                    TestItem::new(cx)
5402                        .with_singleton(true)
5403                        .with_label("buffer 1")
5404                        .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
5405                })),
5406                false,
5407                false,
5408                None,
5409                window,
5410                cx,
5411            );
5412        });
5413        assert_item_labels(&pane, ["buffer 1*"], cx);
5414
5415        // new singleton view with different project entry
5416        pane.update_in(cx, |pane, window, cx| {
5417            pane.add_item(
5418                Box::new(cx.new(|cx| {
5419                    TestItem::new(cx)
5420                        .with_singleton(true)
5421                        .with_label("buffer 2")
5422                        .with_project_items(&[TestProjectItem::new(2, "2.txt", cx)])
5423                })),
5424                false,
5425                false,
5426                None,
5427                window,
5428                cx,
5429            );
5430        });
5431        assert_item_labels(&pane, ["buffer 1", "buffer 2*"], cx);
5432
5433        // new multibuffer view with the same project entry
5434        pane.update_in(cx, |pane, window, cx| {
5435            pane.add_item(
5436                Box::new(cx.new(|cx| {
5437                    TestItem::new(cx)
5438                        .with_singleton(false)
5439                        .with_label("multibuffer 1")
5440                        .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
5441                })),
5442                false,
5443                false,
5444                None,
5445                window,
5446                cx,
5447            );
5448        });
5449        assert_item_labels(&pane, ["buffer 1", "buffer 2", "multibuffer 1*"], cx);
5450
5451        // another multibuffer view with the same project entry
5452        pane.update_in(cx, |pane, window, cx| {
5453            pane.add_item(
5454                Box::new(cx.new(|cx| {
5455                    TestItem::new(cx)
5456                        .with_singleton(false)
5457                        .with_label("multibuffer 1b")
5458                        .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
5459                })),
5460                false,
5461                false,
5462                None,
5463                window,
5464                cx,
5465            );
5466        });
5467        assert_item_labels(
5468            &pane,
5469            ["buffer 1", "buffer 2", "multibuffer 1", "multibuffer 1b*"],
5470            cx,
5471        );
5472    }
5473
5474    #[gpui::test]
5475    async fn test_remove_item_ordering_history(cx: &mut TestAppContext) {
5476        init_test(cx);
5477        let fs = FakeFs::new(cx.executor());
5478
5479        let project = Project::test(fs, None, cx).await;
5480        let (workspace, cx) =
5481            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
5482        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
5483
5484        add_labeled_item(&pane, "A", false, cx);
5485        add_labeled_item(&pane, "B", false, cx);
5486        add_labeled_item(&pane, "C", false, cx);
5487        add_labeled_item(&pane, "D", false, cx);
5488        assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
5489
5490        pane.update_in(cx, |pane, window, cx| {
5491            pane.activate_item(1, false, false, window, cx)
5492        });
5493        add_labeled_item(&pane, "1", false, cx);
5494        assert_item_labels(&pane, ["A", "B", "1*", "C", "D"], cx);
5495
5496        pane.update_in(cx, |pane, window, cx| {
5497            pane.close_active_item(
5498                &CloseActiveItem {
5499                    save_intent: None,
5500                    close_pinned: false,
5501                },
5502                window,
5503                cx,
5504            )
5505        })
5506        .await
5507        .unwrap();
5508        assert_item_labels(&pane, ["A", "B*", "C", "D"], cx);
5509
5510        pane.update_in(cx, |pane, window, cx| {
5511            pane.activate_item(3, false, false, window, cx)
5512        });
5513        assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
5514
5515        pane.update_in(cx, |pane, window, cx| {
5516            pane.close_active_item(
5517                &CloseActiveItem {
5518                    save_intent: None,
5519                    close_pinned: false,
5520                },
5521                window,
5522                cx,
5523            )
5524        })
5525        .await
5526        .unwrap();
5527        assert_item_labels(&pane, ["A", "B*", "C"], cx);
5528
5529        pane.update_in(cx, |pane, window, cx| {
5530            pane.close_active_item(
5531                &CloseActiveItem {
5532                    save_intent: None,
5533                    close_pinned: false,
5534                },
5535                window,
5536                cx,
5537            )
5538        })
5539        .await
5540        .unwrap();
5541        assert_item_labels(&pane, ["A", "C*"], cx);
5542
5543        pane.update_in(cx, |pane, window, cx| {
5544            pane.close_active_item(
5545                &CloseActiveItem {
5546                    save_intent: None,
5547                    close_pinned: false,
5548                },
5549                window,
5550                cx,
5551            )
5552        })
5553        .await
5554        .unwrap();
5555        assert_item_labels(&pane, ["A*"], cx);
5556    }
5557
5558    #[gpui::test]
5559    async fn test_remove_item_ordering_neighbour(cx: &mut TestAppContext) {
5560        init_test(cx);
5561        cx.update_global::<SettingsStore, ()>(|s, cx| {
5562            s.update_user_settings::<ItemSettings>(cx, |s| {
5563                s.activate_on_close = Some(ActivateOnClose::Neighbour);
5564            });
5565        });
5566        let fs = FakeFs::new(cx.executor());
5567
5568        let project = Project::test(fs, None, cx).await;
5569        let (workspace, cx) =
5570            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
5571        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
5572
5573        add_labeled_item(&pane, "A", false, cx);
5574        add_labeled_item(&pane, "B", false, cx);
5575        add_labeled_item(&pane, "C", false, cx);
5576        add_labeled_item(&pane, "D", false, cx);
5577        assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
5578
5579        pane.update_in(cx, |pane, window, cx| {
5580            pane.activate_item(1, false, false, window, cx)
5581        });
5582        add_labeled_item(&pane, "1", false, cx);
5583        assert_item_labels(&pane, ["A", "B", "1*", "C", "D"], cx);
5584
5585        pane.update_in(cx, |pane, window, cx| {
5586            pane.close_active_item(
5587                &CloseActiveItem {
5588                    save_intent: None,
5589                    close_pinned: false,
5590                },
5591                window,
5592                cx,
5593            )
5594        })
5595        .await
5596        .unwrap();
5597        assert_item_labels(&pane, ["A", "B", "C*", "D"], cx);
5598
5599        pane.update_in(cx, |pane, window, cx| {
5600            pane.activate_item(3, false, false, window, cx)
5601        });
5602        assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
5603
5604        pane.update_in(cx, |pane, window, cx| {
5605            pane.close_active_item(
5606                &CloseActiveItem {
5607                    save_intent: None,
5608                    close_pinned: false,
5609                },
5610                window,
5611                cx,
5612            )
5613        })
5614        .await
5615        .unwrap();
5616        assert_item_labels(&pane, ["A", "B", "C*"], cx);
5617
5618        pane.update_in(cx, |pane, window, cx| {
5619            pane.close_active_item(
5620                &CloseActiveItem {
5621                    save_intent: None,
5622                    close_pinned: false,
5623                },
5624                window,
5625                cx,
5626            )
5627        })
5628        .await
5629        .unwrap();
5630        assert_item_labels(&pane, ["A", "B*"], cx);
5631
5632        pane.update_in(cx, |pane, window, cx| {
5633            pane.close_active_item(
5634                &CloseActiveItem {
5635                    save_intent: None,
5636                    close_pinned: false,
5637                },
5638                window,
5639                cx,
5640            )
5641        })
5642        .await
5643        .unwrap();
5644        assert_item_labels(&pane, ["A*"], cx);
5645    }
5646
5647    #[gpui::test]
5648    async fn test_remove_item_ordering_left_neighbour(cx: &mut TestAppContext) {
5649        init_test(cx);
5650        cx.update_global::<SettingsStore, ()>(|s, cx| {
5651            s.update_user_settings::<ItemSettings>(cx, |s| {
5652                s.activate_on_close = Some(ActivateOnClose::LeftNeighbour);
5653            });
5654        });
5655        let fs = FakeFs::new(cx.executor());
5656
5657        let project = Project::test(fs, None, cx).await;
5658        let (workspace, cx) =
5659            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
5660        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
5661
5662        add_labeled_item(&pane, "A", false, cx);
5663        add_labeled_item(&pane, "B", false, cx);
5664        add_labeled_item(&pane, "C", false, cx);
5665        add_labeled_item(&pane, "D", false, cx);
5666        assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
5667
5668        pane.update_in(cx, |pane, window, cx| {
5669            pane.activate_item(1, false, false, window, cx)
5670        });
5671        add_labeled_item(&pane, "1", false, cx);
5672        assert_item_labels(&pane, ["A", "B", "1*", "C", "D"], cx);
5673
5674        pane.update_in(cx, |pane, window, cx| {
5675            pane.close_active_item(
5676                &CloseActiveItem {
5677                    save_intent: None,
5678                    close_pinned: false,
5679                },
5680                window,
5681                cx,
5682            )
5683        })
5684        .await
5685        .unwrap();
5686        assert_item_labels(&pane, ["A", "B*", "C", "D"], cx);
5687
5688        pane.update_in(cx, |pane, window, cx| {
5689            pane.activate_item(3, false, false, window, cx)
5690        });
5691        assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
5692
5693        pane.update_in(cx, |pane, window, cx| {
5694            pane.close_active_item(
5695                &CloseActiveItem {
5696                    save_intent: None,
5697                    close_pinned: false,
5698                },
5699                window,
5700                cx,
5701            )
5702        })
5703        .await
5704        .unwrap();
5705        assert_item_labels(&pane, ["A", "B", "C*"], cx);
5706
5707        pane.update_in(cx, |pane, window, cx| {
5708            pane.activate_item(0, false, false, window, cx)
5709        });
5710        assert_item_labels(&pane, ["A*", "B", "C"], cx);
5711
5712        pane.update_in(cx, |pane, window, cx| {
5713            pane.close_active_item(
5714                &CloseActiveItem {
5715                    save_intent: None,
5716                    close_pinned: false,
5717                },
5718                window,
5719                cx,
5720            )
5721        })
5722        .await
5723        .unwrap();
5724        assert_item_labels(&pane, ["B*", "C"], cx);
5725
5726        pane.update_in(cx, |pane, window, cx| {
5727            pane.close_active_item(
5728                &CloseActiveItem {
5729                    save_intent: None,
5730                    close_pinned: false,
5731                },
5732                window,
5733                cx,
5734            )
5735        })
5736        .await
5737        .unwrap();
5738        assert_item_labels(&pane, ["C*"], cx);
5739    }
5740
5741    #[gpui::test]
5742    async fn test_close_inactive_items(cx: &mut TestAppContext) {
5743        init_test(cx);
5744        let fs = FakeFs::new(cx.executor());
5745
5746        let project = Project::test(fs, None, cx).await;
5747        let (workspace, cx) =
5748            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
5749        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
5750
5751        let item_a = add_labeled_item(&pane, "A", false, cx);
5752        pane.update_in(cx, |pane, window, cx| {
5753            let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
5754            pane.pin_tab_at(ix, window, cx);
5755        });
5756        assert_item_labels(&pane, ["A*!"], cx);
5757
5758        let item_b = add_labeled_item(&pane, "B", false, cx);
5759        pane.update_in(cx, |pane, window, cx| {
5760            let ix = pane.index_for_item_id(item_b.item_id()).unwrap();
5761            pane.pin_tab_at(ix, window, cx);
5762        });
5763        assert_item_labels(&pane, ["A!", "B*!"], cx);
5764
5765        add_labeled_item(&pane, "C", false, cx);
5766        assert_item_labels(&pane, ["A!", "B!", "C*"], cx);
5767
5768        add_labeled_item(&pane, "D", false, cx);
5769        add_labeled_item(&pane, "E", false, cx);
5770        assert_item_labels(&pane, ["A!", "B!", "C", "D", "E*"], cx);
5771
5772        pane.update_in(cx, |pane, window, cx| {
5773            pane.close_inactive_items(
5774                &CloseInactiveItems {
5775                    save_intent: None,
5776                    close_pinned: false,
5777                },
5778                window,
5779                cx,
5780            )
5781        })
5782        .await
5783        .unwrap();
5784        assert_item_labels(&pane, ["A!", "B!", "E*"], cx);
5785    }
5786
5787    #[gpui::test]
5788    async fn test_close_clean_items(cx: &mut TestAppContext) {
5789        init_test(cx);
5790        let fs = FakeFs::new(cx.executor());
5791
5792        let project = Project::test(fs, None, cx).await;
5793        let (workspace, cx) =
5794            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
5795        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
5796
5797        add_labeled_item(&pane, "A", true, cx);
5798        add_labeled_item(&pane, "B", false, cx);
5799        add_labeled_item(&pane, "C", true, cx);
5800        add_labeled_item(&pane, "D", false, cx);
5801        add_labeled_item(&pane, "E", false, cx);
5802        assert_item_labels(&pane, ["A^", "B", "C^", "D", "E*"], cx);
5803
5804        pane.update_in(cx, |pane, window, cx| {
5805            pane.close_clean_items(
5806                &CloseCleanItems {
5807                    close_pinned: false,
5808                },
5809                window,
5810                cx,
5811            )
5812        })
5813        .await
5814        .unwrap();
5815        assert_item_labels(&pane, ["A^", "C*^"], cx);
5816    }
5817
5818    #[gpui::test]
5819    async fn test_close_items_to_the_left(cx: &mut TestAppContext) {
5820        init_test(cx);
5821        let fs = FakeFs::new(cx.executor());
5822
5823        let project = Project::test(fs, None, cx).await;
5824        let (workspace, cx) =
5825            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
5826        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
5827
5828        set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx);
5829
5830        pane.update_in(cx, |pane, window, cx| {
5831            pane.close_items_to_the_left_by_id(
5832                None,
5833                &CloseItemsToTheLeft {
5834                    close_pinned: false,
5835                },
5836                window,
5837                cx,
5838            )
5839        })
5840        .await
5841        .unwrap();
5842        assert_item_labels(&pane, ["C*", "D", "E"], cx);
5843    }
5844
5845    #[gpui::test]
5846    async fn test_close_items_to_the_right(cx: &mut TestAppContext) {
5847        init_test(cx);
5848        let fs = FakeFs::new(cx.executor());
5849
5850        let project = Project::test(fs, None, cx).await;
5851        let (workspace, cx) =
5852            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
5853        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
5854
5855        set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx);
5856
5857        pane.update_in(cx, |pane, window, cx| {
5858            pane.close_items_to_the_right_by_id(
5859                None,
5860                &CloseItemsToTheRight {
5861                    close_pinned: false,
5862                },
5863                window,
5864                cx,
5865            )
5866        })
5867        .await
5868        .unwrap();
5869        assert_item_labels(&pane, ["A", "B", "C*"], cx);
5870    }
5871
5872    #[gpui::test]
5873    async fn test_close_all_items(cx: &mut TestAppContext) {
5874        init_test(cx);
5875        let fs = FakeFs::new(cx.executor());
5876
5877        let project = Project::test(fs, None, cx).await;
5878        let (workspace, cx) =
5879            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
5880        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
5881
5882        let item_a = add_labeled_item(&pane, "A", false, cx);
5883        add_labeled_item(&pane, "B", false, cx);
5884        add_labeled_item(&pane, "C", false, cx);
5885        assert_item_labels(&pane, ["A", "B", "C*"], cx);
5886
5887        pane.update_in(cx, |pane, window, cx| {
5888            let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
5889            pane.pin_tab_at(ix, window, cx);
5890            pane.close_all_items(
5891                &CloseAllItems {
5892                    save_intent: None,
5893                    close_pinned: false,
5894                },
5895                window,
5896                cx,
5897            )
5898        })
5899        .await
5900        .unwrap();
5901        assert_item_labels(&pane, ["A*!"], cx);
5902
5903        pane.update_in(cx, |pane, window, cx| {
5904            let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
5905            pane.unpin_tab_at(ix, window, cx);
5906            pane.close_all_items(
5907                &CloseAllItems {
5908                    save_intent: None,
5909                    close_pinned: false,
5910                },
5911                window,
5912                cx,
5913            )
5914        })
5915        .await
5916        .unwrap();
5917
5918        assert_item_labels(&pane, [], cx);
5919
5920        add_labeled_item(&pane, "A", true, cx).update(cx, |item, cx| {
5921            item.project_items
5922                .push(TestProjectItem::new_dirty(1, "A.txt", cx))
5923        });
5924        add_labeled_item(&pane, "B", true, cx).update(cx, |item, cx| {
5925            item.project_items
5926                .push(TestProjectItem::new_dirty(2, "B.txt", cx))
5927        });
5928        add_labeled_item(&pane, "C", true, cx).update(cx, |item, cx| {
5929            item.project_items
5930                .push(TestProjectItem::new_dirty(3, "C.txt", cx))
5931        });
5932        assert_item_labels(&pane, ["A^", "B^", "C*^"], cx);
5933
5934        let save = pane.update_in(cx, |pane, window, cx| {
5935            pane.close_all_items(
5936                &CloseAllItems {
5937                    save_intent: None,
5938                    close_pinned: false,
5939                },
5940                window,
5941                cx,
5942            )
5943        });
5944
5945        cx.executor().run_until_parked();
5946        cx.simulate_prompt_answer("Save all");
5947        save.await.unwrap();
5948        assert_item_labels(&pane, [], cx);
5949
5950        add_labeled_item(&pane, "A", true, cx);
5951        add_labeled_item(&pane, "B", true, cx);
5952        add_labeled_item(&pane, "C", true, cx);
5953        assert_item_labels(&pane, ["A^", "B^", "C*^"], cx);
5954        let save = pane.update_in(cx, |pane, window, cx| {
5955            pane.close_all_items(
5956                &CloseAllItems {
5957                    save_intent: None,
5958                    close_pinned: false,
5959                },
5960                window,
5961                cx,
5962            )
5963        });
5964
5965        cx.executor().run_until_parked();
5966        cx.simulate_prompt_answer("Discard all");
5967        save.await.unwrap();
5968        assert_item_labels(&pane, [], cx);
5969    }
5970
5971    #[gpui::test]
5972    async fn test_close_with_save_intent(cx: &mut TestAppContext) {
5973        init_test(cx);
5974        let fs = FakeFs::new(cx.executor());
5975
5976        let project = Project::test(fs, None, cx).await;
5977        let (workspace, cx) =
5978            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
5979        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
5980
5981        let a = cx.update(|_, cx| TestProjectItem::new_dirty(1, "A.txt", cx));
5982        let b = cx.update(|_, cx| TestProjectItem::new_dirty(1, "B.txt", cx));
5983        let c = cx.update(|_, cx| TestProjectItem::new_dirty(1, "C.txt", cx));
5984
5985        add_labeled_item(&pane, "AB", true, cx).update(cx, |item, _| {
5986            item.project_items.push(a.clone());
5987            item.project_items.push(b.clone());
5988        });
5989        add_labeled_item(&pane, "C", true, cx)
5990            .update(cx, |item, _| item.project_items.push(c.clone()));
5991        assert_item_labels(&pane, ["AB^", "C*^"], cx);
5992
5993        pane.update_in(cx, |pane, window, cx| {
5994            pane.close_all_items(
5995                &CloseAllItems {
5996                    save_intent: Some(SaveIntent::Save),
5997                    close_pinned: false,
5998                },
5999                window,
6000                cx,
6001            )
6002        })
6003        .await
6004        .unwrap();
6005
6006        assert_item_labels(&pane, [], cx);
6007        cx.update(|_, cx| {
6008            assert!(!a.read(cx).is_dirty);
6009            assert!(!b.read(cx).is_dirty);
6010            assert!(!c.read(cx).is_dirty);
6011        });
6012    }
6013
6014    #[gpui::test]
6015    async fn test_close_all_items_including_pinned(cx: &mut TestAppContext) {
6016        init_test(cx);
6017        let fs = FakeFs::new(cx.executor());
6018
6019        let project = Project::test(fs, None, cx).await;
6020        let (workspace, cx) =
6021            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
6022        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
6023
6024        let item_a = add_labeled_item(&pane, "A", false, cx);
6025        add_labeled_item(&pane, "B", false, cx);
6026        add_labeled_item(&pane, "C", false, cx);
6027        assert_item_labels(&pane, ["A", "B", "C*"], cx);
6028
6029        pane.update_in(cx, |pane, window, cx| {
6030            let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
6031            pane.pin_tab_at(ix, window, cx);
6032            pane.close_all_items(
6033                &CloseAllItems {
6034                    save_intent: None,
6035                    close_pinned: true,
6036                },
6037                window,
6038                cx,
6039            )
6040        })
6041        .await
6042        .unwrap();
6043        assert_item_labels(&pane, [], cx);
6044    }
6045
6046    #[gpui::test]
6047    async fn test_close_pinned_tab_with_non_pinned_in_same_pane(cx: &mut TestAppContext) {
6048        init_test(cx);
6049        let fs = FakeFs::new(cx.executor());
6050        let project = Project::test(fs, None, cx).await;
6051        let (workspace, cx) =
6052            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
6053
6054        // Non-pinned tabs in same pane
6055        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
6056        add_labeled_item(&pane, "A", false, cx);
6057        add_labeled_item(&pane, "B", false, cx);
6058        add_labeled_item(&pane, "C", false, cx);
6059        pane.update_in(cx, |pane, window, cx| {
6060            pane.pin_tab_at(0, window, cx);
6061        });
6062        set_labeled_items(&pane, ["A*", "B", "C"], cx);
6063        pane.update_in(cx, |pane, window, cx| {
6064            pane.close_active_item(
6065                &CloseActiveItem {
6066                    save_intent: None,
6067                    close_pinned: false,
6068                },
6069                window,
6070                cx,
6071            )
6072            .unwrap();
6073        });
6074        // Non-pinned tab should be active
6075        assert_item_labels(&pane, ["A!", "B*", "C"], cx);
6076    }
6077
6078    #[gpui::test]
6079    async fn test_close_pinned_tab_with_non_pinned_in_different_pane(cx: &mut TestAppContext) {
6080        init_test(cx);
6081        let fs = FakeFs::new(cx.executor());
6082        let project = Project::test(fs, None, cx).await;
6083        let (workspace, cx) =
6084            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
6085
6086        // No non-pinned tabs in same pane, non-pinned tabs in another pane
6087        let pane1 = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
6088        let pane2 = workspace.update_in(cx, |workspace, window, cx| {
6089            workspace.split_pane(pane1.clone(), SplitDirection::Right, window, cx)
6090        });
6091        add_labeled_item(&pane1, "A", false, cx);
6092        pane1.update_in(cx, |pane, window, cx| {
6093            pane.pin_tab_at(0, window, cx);
6094        });
6095        set_labeled_items(&pane1, ["A*"], cx);
6096        add_labeled_item(&pane2, "B", false, cx);
6097        set_labeled_items(&pane2, ["B"], cx);
6098        pane1.update_in(cx, |pane, window, cx| {
6099            pane.close_active_item(
6100                &CloseActiveItem {
6101                    save_intent: None,
6102                    close_pinned: false,
6103                },
6104                window,
6105                cx,
6106            )
6107            .unwrap();
6108        });
6109        //  Non-pinned tab of other pane should be active
6110        assert_item_labels(&pane2, ["B*"], cx);
6111    }
6112
6113    #[gpui::test]
6114    async fn ensure_item_closing_actions_do_not_panic_when_no_items_exist(cx: &mut TestAppContext) {
6115        init_test(cx);
6116        let fs = FakeFs::new(cx.executor());
6117        let project = Project::test(fs, None, cx).await;
6118        let (workspace, cx) =
6119            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
6120
6121        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
6122        assert_item_labels(&pane, [], cx);
6123
6124        pane.update_in(cx, |pane, window, cx| {
6125            pane.close_active_item(
6126                &CloseActiveItem {
6127                    save_intent: None,
6128                    close_pinned: false,
6129                },
6130                window,
6131                cx,
6132            )
6133        })
6134        .await
6135        .unwrap();
6136
6137        pane.update_in(cx, |pane, window, cx| {
6138            pane.close_inactive_items(
6139                &CloseInactiveItems {
6140                    save_intent: None,
6141                    close_pinned: false,
6142                },
6143                window,
6144                cx,
6145            )
6146        })
6147        .await
6148        .unwrap();
6149
6150        pane.update_in(cx, |pane, window, cx| {
6151            pane.close_all_items(
6152                &CloseAllItems {
6153                    save_intent: None,
6154                    close_pinned: false,
6155                },
6156                window,
6157                cx,
6158            )
6159        })
6160        .await
6161        .unwrap();
6162
6163        pane.update_in(cx, |pane, window, cx| {
6164            pane.close_clean_items(
6165                &CloseCleanItems {
6166                    close_pinned: false,
6167                },
6168                window,
6169                cx,
6170            )
6171        })
6172        .await
6173        .unwrap();
6174
6175        pane.update_in(cx, |pane, window, cx| {
6176            pane.close_items_to_the_right_by_id(
6177                None,
6178                &CloseItemsToTheRight {
6179                    close_pinned: false,
6180                },
6181                window,
6182                cx,
6183            )
6184        })
6185        .await
6186        .unwrap();
6187
6188        pane.update_in(cx, |pane, window, cx| {
6189            pane.close_items_to_the_left_by_id(
6190                None,
6191                &CloseItemsToTheLeft {
6192                    close_pinned: false,
6193                },
6194                window,
6195                cx,
6196            )
6197        })
6198        .await
6199        .unwrap();
6200    }
6201
6202    fn init_test(cx: &mut TestAppContext) {
6203        cx.update(|cx| {
6204            let settings_store = SettingsStore::test(cx);
6205            cx.set_global(settings_store);
6206            theme::init(LoadThemes::JustBase, cx);
6207            crate::init_settings(cx);
6208            Project::init_settings(cx);
6209        });
6210    }
6211
6212    fn set_max_tabs(cx: &mut TestAppContext, value: Option<usize>) {
6213        cx.update_global(|store: &mut SettingsStore, cx| {
6214            store.update_user_settings::<WorkspaceSettings>(cx, |settings| {
6215                settings.max_tabs = value.map(|v| NonZero::new(v).unwrap())
6216            });
6217        });
6218    }
6219
6220    fn add_labeled_item(
6221        pane: &Entity<Pane>,
6222        label: &str,
6223        is_dirty: bool,
6224        cx: &mut VisualTestContext,
6225    ) -> Box<Entity<TestItem>> {
6226        pane.update_in(cx, |pane, window, cx| {
6227            let labeled_item =
6228                Box::new(cx.new(|cx| TestItem::new(cx).with_label(label).with_dirty(is_dirty)));
6229            pane.add_item(labeled_item.clone(), false, false, None, window, cx);
6230            labeled_item
6231        })
6232    }
6233
6234    fn set_labeled_items<const COUNT: usize>(
6235        pane: &Entity<Pane>,
6236        labels: [&str; COUNT],
6237        cx: &mut VisualTestContext,
6238    ) -> [Box<Entity<TestItem>>; COUNT] {
6239        pane.update_in(cx, |pane, window, cx| {
6240            pane.items.clear();
6241            let mut active_item_index = 0;
6242
6243            let mut index = 0;
6244            let items = labels.map(|mut label| {
6245                if label.ends_with('*') {
6246                    label = label.trim_end_matches('*');
6247                    active_item_index = index;
6248                }
6249
6250                let labeled_item = Box::new(cx.new(|cx| TestItem::new(cx).with_label(label)));
6251                pane.add_item(labeled_item.clone(), false, false, None, window, cx);
6252                index += 1;
6253                labeled_item
6254            });
6255
6256            pane.activate_item(active_item_index, false, false, window, cx);
6257
6258            items
6259        })
6260    }
6261
6262    // Assert the item label, with the active item label suffixed with a '*'
6263    #[track_caller]
6264    fn assert_item_labels<const COUNT: usize>(
6265        pane: &Entity<Pane>,
6266        expected_states: [&str; COUNT],
6267        cx: &mut VisualTestContext,
6268    ) {
6269        let actual_states = pane.update(cx, |pane, cx| {
6270            pane.items
6271                .iter()
6272                .enumerate()
6273                .map(|(ix, item)| {
6274                    let mut state = item
6275                        .to_any()
6276                        .downcast::<TestItem>()
6277                        .unwrap()
6278                        .read(cx)
6279                        .label
6280                        .clone();
6281                    if ix == pane.active_item_index {
6282                        state.push('*');
6283                    }
6284                    if item.is_dirty(cx) {
6285                        state.push('^');
6286                    }
6287                    if pane.is_tab_pinned(ix) {
6288                        state.push('!');
6289                    }
6290                    state
6291                })
6292                .collect::<Vec<_>>()
6293        });
6294        assert_eq!(
6295            actual_states, expected_states,
6296            "pane items do not match expectation"
6297        );
6298    }
6299}