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