pane.rs

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