pane.rs

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