pane.rs

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