pane.rs

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