pane.rs

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