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