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        target_item_id: Option<EntityId>,
1350        window: &mut Window,
1351        cx: &mut Context<Self>,
1352    ) -> Task<Result<()>> {
1353        if self.items.is_empty() {
1354            return Task::ready(Ok(()));
1355        }
1356
1357        let active_item_id = match target_item_id {
1358            Some(result) => result,
1359            None => self.active_item_id(),
1360        };
1361
1362        let pinned_item_ids = self.pinned_item_ids();
1363
1364        self.close_items(
1365            window,
1366            cx,
1367            action.save_intent.unwrap_or(SaveIntent::Close),
1368            move |item_id| {
1369                item_id != active_item_id
1370                    && (action.close_pinned || !pinned_item_ids.contains(&item_id))
1371            },
1372        )
1373    }
1374
1375    pub fn close_clean_items(
1376        &mut self,
1377        action: &CloseCleanItems,
1378        window: &mut Window,
1379        cx: &mut Context<Self>,
1380    ) -> Task<Result<()>> {
1381        if self.items.is_empty() {
1382            return Task::ready(Ok(()));
1383        }
1384
1385        let clean_item_ids = self.clean_item_ids(cx);
1386        let pinned_item_ids = self.pinned_item_ids();
1387
1388        self.close_items(window, cx, SaveIntent::Close, move |item_id| {
1389            clean_item_ids.contains(&item_id)
1390                && (action.close_pinned || !pinned_item_ids.contains(&item_id))
1391        })
1392    }
1393
1394    pub fn close_items_to_the_left_by_id(
1395        &mut self,
1396        item_id: Option<EntityId>,
1397        action: &CloseItemsToTheLeft,
1398        window: &mut Window,
1399        cx: &mut Context<Self>,
1400    ) -> Task<Result<()>> {
1401        self.close_items_to_the_side_by_id(item_id, Side::Left, action.close_pinned, window, cx)
1402    }
1403
1404    pub fn close_items_to_the_right_by_id(
1405        &mut self,
1406        item_id: Option<EntityId>,
1407        action: &CloseItemsToTheRight,
1408        window: &mut Window,
1409        cx: &mut Context<Self>,
1410    ) -> Task<Result<()>> {
1411        self.close_items_to_the_side_by_id(item_id, Side::Right, action.close_pinned, window, cx)
1412    }
1413
1414    pub fn close_items_to_the_side_by_id(
1415        &mut self,
1416        item_id: Option<EntityId>,
1417        side: Side,
1418        close_pinned: bool,
1419        window: &mut Window,
1420        cx: &mut Context<Self>,
1421    ) -> Task<Result<()>> {
1422        if self.items.is_empty() {
1423            return Task::ready(Ok(()));
1424        }
1425
1426        let item_id = item_id.unwrap_or_else(|| self.active_item_id());
1427        let to_the_side_item_ids = self.to_the_side_item_ids(item_id, side);
1428        let pinned_item_ids = self.pinned_item_ids();
1429
1430        self.close_items(window, cx, SaveIntent::Close, move |item_id| {
1431            to_the_side_item_ids.contains(&item_id)
1432                && (close_pinned || !pinned_item_ids.contains(&item_id))
1433        })
1434    }
1435
1436    pub fn close_all_items(
1437        &mut self,
1438        action: &CloseAllItems,
1439        window: &mut Window,
1440        cx: &mut Context<Self>,
1441    ) -> Task<Result<()>> {
1442        if self.items.is_empty() {
1443            return Task::ready(Ok(()));
1444        }
1445
1446        let pinned_item_ids = self.pinned_item_ids();
1447
1448        self.close_items(
1449            window,
1450            cx,
1451            action.save_intent.unwrap_or(SaveIntent::Close),
1452            |item_id| action.close_pinned || !pinned_item_ids.contains(&item_id),
1453        )
1454    }
1455
1456    fn close_items_on_item_open(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1457        let target = self.max_tabs.map(|m| m.get());
1458        let protect_active_item = false;
1459        self.close_items_to_target_count(target, protect_active_item, window, cx);
1460    }
1461
1462    fn close_items_on_settings_change(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1463        let target = self.max_tabs.map(|m| m.get() + 1);
1464        // The active item in this case is the settings.json file, which should be protected from being closed
1465        let protect_active_item = true;
1466        self.close_items_to_target_count(target, protect_active_item, window, cx);
1467    }
1468
1469    fn close_items_to_target_count(
1470        &mut self,
1471        target_count: Option<usize>,
1472        protect_active_item: bool,
1473        window: &mut Window,
1474        cx: &mut Context<Self>,
1475    ) {
1476        let Some(target_count) = target_count else {
1477            return;
1478        };
1479
1480        let mut index_list = Vec::new();
1481        let mut items_len = self.items_len();
1482        let mut indexes: HashMap<EntityId, usize> = HashMap::default();
1483        let active_ix = self.active_item_index();
1484
1485        for (index, item) in self.items.iter().enumerate() {
1486            indexes.insert(item.item_id(), index);
1487        }
1488
1489        // Close least recently used items to reach target count.
1490        // The target count is allowed to be exceeded, as we protect pinned
1491        // items, dirty items, and sometimes, the active item.
1492        for entry in self.activation_history.iter() {
1493            if items_len < target_count {
1494                break;
1495            }
1496
1497            let Some(&index) = indexes.get(&entry.entity_id) else {
1498                continue;
1499            };
1500
1501            if protect_active_item && index == active_ix {
1502                continue;
1503            }
1504
1505            if let Some(true) = self.items.get(index).map(|item| item.is_dirty(cx)) {
1506                continue;
1507            }
1508
1509            if self.is_tab_pinned(index) {
1510                continue;
1511            }
1512
1513            index_list.push(index);
1514            items_len -= 1;
1515        }
1516        // The sort and reverse is necessary since we remove items
1517        // using their index position, hence removing from the end
1518        // of the list first to avoid changing indexes.
1519        index_list.sort_unstable();
1520        index_list
1521            .iter()
1522            .rev()
1523            .for_each(|&index| self._remove_item(index, false, false, None, window, cx));
1524    }
1525
1526    // Usually when you close an item that has unsaved changes, we prompt you to
1527    // save it. That said, if you still have the buffer open in a different pane
1528    // we can close this one without fear of losing data.
1529    pub fn skip_save_on_close(item: &dyn ItemHandle, workspace: &Workspace, cx: &App) -> bool {
1530        let mut dirty_project_item_ids = Vec::new();
1531        item.for_each_project_item(cx, &mut |project_item_id, project_item| {
1532            if project_item.is_dirty() {
1533                dirty_project_item_ids.push(project_item_id);
1534            }
1535        });
1536        if dirty_project_item_ids.is_empty() {
1537            return !(item.is_singleton(cx) && item.is_dirty(cx));
1538        }
1539
1540        for open_item in workspace.items(cx) {
1541            if open_item.item_id() == item.item_id() {
1542                continue;
1543            }
1544            if !open_item.is_singleton(cx) {
1545                continue;
1546            }
1547            let other_project_item_ids = open_item.project_item_model_ids(cx);
1548            dirty_project_item_ids.retain(|id| !other_project_item_ids.contains(id));
1549        }
1550        return dirty_project_item_ids.is_empty();
1551    }
1552
1553    pub(super) fn file_names_for_prompt(
1554        items: &mut dyn Iterator<Item = &Box<dyn ItemHandle>>,
1555        cx: &App,
1556    ) -> String {
1557        let mut file_names = BTreeSet::default();
1558        for item in items {
1559            item.for_each_project_item(cx, &mut |_, project_item| {
1560                if !project_item.is_dirty() {
1561                    return;
1562                }
1563                let filename = project_item.project_path(cx).and_then(|path| {
1564                    path.path
1565                        .file_name()
1566                        .and_then(|name| name.to_str().map(ToOwned::to_owned))
1567                });
1568                file_names.insert(filename.unwrap_or("untitled".to_string()));
1569            });
1570        }
1571        if file_names.len() > 6 {
1572            format!(
1573                "{}\n.. and {} more",
1574                file_names.iter().take(5).join("\n"),
1575                file_names.len() - 5
1576            )
1577        } else {
1578            file_names.into_iter().join("\n")
1579        }
1580    }
1581
1582    pub fn close_items(
1583        &self,
1584        window: &mut Window,
1585        cx: &mut Context<Pane>,
1586        mut save_intent: SaveIntent,
1587        should_close: impl Fn(EntityId) -> bool,
1588    ) -> Task<Result<()>> {
1589        // Find the items to close.
1590        let mut items_to_close = Vec::new();
1591        for item in &self.items {
1592            if should_close(item.item_id()) {
1593                items_to_close.push(item.boxed_clone());
1594            }
1595        }
1596
1597        let active_item_id = self.active_item().map(|item| item.item_id());
1598
1599        items_to_close.sort_by_key(|item| {
1600            let path = item.project_path(cx);
1601            // Put the currently active item at the end, because if the currently active item is not closed last
1602            // closing the currently active item will cause the focus to switch to another item
1603            // This will cause Zed to expand the content of the currently active item
1604            //
1605            // Beyond that sort in order of project path, with untitled files and multibuffers coming last.
1606            (active_item_id == Some(item.item_id()), path.is_none(), path)
1607        });
1608
1609        let workspace = self.workspace.clone();
1610        let Some(project) = self.project.upgrade() else {
1611            return Task::ready(Ok(()));
1612        };
1613        cx.spawn_in(window, async move |pane, cx| {
1614            let dirty_items = workspace.update(cx, |workspace, cx| {
1615                items_to_close
1616                    .iter()
1617                    .filter(|item| {
1618                        item.is_dirty(cx)
1619                            && !Self::skip_save_on_close(item.as_ref(), &workspace, cx)
1620                    })
1621                    .map(|item| item.boxed_clone())
1622                    .collect::<Vec<_>>()
1623            })?;
1624
1625            if save_intent == SaveIntent::Close && dirty_items.len() > 1 {
1626                let answer = pane.update_in(cx, |_, window, cx| {
1627                    let detail = Self::file_names_for_prompt(&mut dirty_items.iter(), cx);
1628                    window.prompt(
1629                        PromptLevel::Warning,
1630                        "Do you want to save changes to the following files?",
1631                        Some(&detail),
1632                        &["Save all", "Discard all", "Cancel"],
1633                        cx,
1634                    )
1635                })?;
1636                match answer.await {
1637                    Ok(0) => save_intent = SaveIntent::SaveAll,
1638                    Ok(1) => save_intent = SaveIntent::Skip,
1639                    Ok(2) => return Ok(()),
1640                    _ => {}
1641                }
1642            }
1643
1644            for item_to_close in items_to_close {
1645                let mut should_save = true;
1646                if save_intent == SaveIntent::Close {
1647                    workspace.update(cx, |workspace, cx| {
1648                        if Self::skip_save_on_close(item_to_close.as_ref(), &workspace, cx) {
1649                            should_save = false;
1650                        }
1651                    })?;
1652                }
1653
1654                if should_save {
1655                    if !Self::save_item(project.clone(), &pane, &*item_to_close, save_intent, cx)
1656                        .await?
1657                    {
1658                        break;
1659                    }
1660                }
1661
1662                // Remove the item from the pane.
1663                pane.update_in(cx, |pane, window, cx| {
1664                    pane.remove_item(
1665                        item_to_close.item_id(),
1666                        false,
1667                        pane.close_pane_if_empty,
1668                        window,
1669                        cx,
1670                    );
1671                })
1672                .ok();
1673            }
1674
1675            pane.update(cx, |_, cx| cx.notify()).ok();
1676            Ok(())
1677        })
1678    }
1679
1680    pub fn remove_item(
1681        &mut self,
1682        item_id: EntityId,
1683        activate_pane: bool,
1684        close_pane_if_empty: bool,
1685        window: &mut Window,
1686        cx: &mut Context<Self>,
1687    ) {
1688        let Some(item_index) = self.index_for_item_id(item_id) else {
1689            return;
1690        };
1691        self._remove_item(
1692            item_index,
1693            activate_pane,
1694            close_pane_if_empty,
1695            None,
1696            window,
1697            cx,
1698        )
1699    }
1700
1701    pub fn remove_item_and_focus_on_pane(
1702        &mut self,
1703        item_index: usize,
1704        activate_pane: bool,
1705        focus_on_pane_if_closed: Entity<Pane>,
1706        window: &mut Window,
1707        cx: &mut Context<Self>,
1708    ) {
1709        self._remove_item(
1710            item_index,
1711            activate_pane,
1712            true,
1713            Some(focus_on_pane_if_closed),
1714            window,
1715            cx,
1716        )
1717    }
1718
1719    fn _remove_item(
1720        &mut self,
1721        item_index: usize,
1722        activate_pane: bool,
1723        close_pane_if_empty: bool,
1724        focus_on_pane_if_closed: Option<Entity<Pane>>,
1725        window: &mut Window,
1726        cx: &mut Context<Self>,
1727    ) {
1728        let activate_on_close = &ItemSettings::get_global(cx).activate_on_close;
1729        self.activation_history
1730            .retain(|entry| entry.entity_id != self.items[item_index].item_id());
1731
1732        if self.is_tab_pinned(item_index) {
1733            self.pinned_tab_count -= 1;
1734        }
1735        if item_index == self.active_item_index {
1736            let left_neighbour_index = || item_index.min(self.items.len()).saturating_sub(1);
1737            let index_to_activate = match activate_on_close {
1738                ActivateOnClose::History => self
1739                    .activation_history
1740                    .pop()
1741                    .and_then(|last_activated_item| {
1742                        self.items.iter().enumerate().find_map(|(index, item)| {
1743                            (item.item_id() == last_activated_item.entity_id).then_some(index)
1744                        })
1745                    })
1746                    // We didn't have a valid activation history entry, so fallback
1747                    // to activating the item to the left
1748                    .unwrap_or_else(left_neighbour_index),
1749                ActivateOnClose::Neighbour => {
1750                    self.activation_history.pop();
1751                    if item_index + 1 < self.items.len() {
1752                        item_index + 1
1753                    } else {
1754                        item_index.saturating_sub(1)
1755                    }
1756                }
1757                ActivateOnClose::LeftNeighbour => {
1758                    self.activation_history.pop();
1759                    left_neighbour_index()
1760                }
1761            };
1762
1763            let should_activate = activate_pane || self.has_focus(window, cx);
1764            if self.items.len() == 1 && should_activate {
1765                self.focus_handle.focus(window);
1766            } else {
1767                self.activate_item(
1768                    index_to_activate,
1769                    should_activate,
1770                    should_activate,
1771                    window,
1772                    cx,
1773                );
1774            }
1775        }
1776
1777        let item = self.items.remove(item_index);
1778
1779        cx.emit(Event::RemovedItem { item: item.clone() });
1780        if self.items.is_empty() {
1781            item.deactivated(window, cx);
1782            if close_pane_if_empty {
1783                self.update_toolbar(window, cx);
1784                cx.emit(Event::Remove {
1785                    focus_on_pane: focus_on_pane_if_closed,
1786                });
1787            }
1788        }
1789
1790        if item_index < self.active_item_index {
1791            self.active_item_index -= 1;
1792        }
1793
1794        let mode = self.nav_history.mode();
1795        self.nav_history.set_mode(NavigationMode::ClosingItem);
1796        item.deactivated(window, cx);
1797        self.nav_history.set_mode(mode);
1798
1799        if self.is_active_preview_item(item.item_id()) {
1800            self.set_preview_item_id(None, cx);
1801        }
1802
1803        if let Some(path) = item.project_path(cx) {
1804            let abs_path = self
1805                .nav_history
1806                .0
1807                .lock()
1808                .paths_by_item
1809                .get(&item.item_id())
1810                .and_then(|(_, abs_path)| abs_path.clone());
1811
1812            self.nav_history
1813                .0
1814                .lock()
1815                .paths_by_item
1816                .insert(item.item_id(), (path, abs_path));
1817        } else {
1818            self.nav_history
1819                .0
1820                .lock()
1821                .paths_by_item
1822                .remove(&item.item_id());
1823        }
1824
1825        if self.zoom_out_on_close && self.items.is_empty() && close_pane_if_empty && self.zoomed {
1826            cx.emit(Event::ZoomOut);
1827        }
1828
1829        cx.notify();
1830    }
1831
1832    pub async fn save_item(
1833        project: Entity<Project>,
1834        pane: &WeakEntity<Pane>,
1835        item: &dyn ItemHandle,
1836        save_intent: SaveIntent,
1837        cx: &mut AsyncWindowContext,
1838    ) -> Result<bool> {
1839        const CONFLICT_MESSAGE: &str = "This file has changed on disk since you started editing it. Do you want to overwrite it?";
1840
1841        const DELETED_MESSAGE: &str = "This file has been deleted on disk since you started editing it. Do you want to recreate it?";
1842
1843        if save_intent == SaveIntent::Skip {
1844            return Ok(true);
1845        }
1846        let Some(item_ix) = pane
1847            .read_with(cx, |pane, _| pane.index_for_item(item))
1848            .ok()
1849            .flatten()
1850        else {
1851            return Ok(true);
1852        };
1853
1854        let (
1855            mut has_conflict,
1856            mut is_dirty,
1857            mut can_save,
1858            can_save_as,
1859            is_singleton,
1860            has_deleted_file,
1861        ) = cx.update(|_window, cx| {
1862            (
1863                item.has_conflict(cx),
1864                item.is_dirty(cx),
1865                item.can_save(cx),
1866                item.can_save_as(cx),
1867                item.is_singleton(cx),
1868                item.has_deleted_file(cx),
1869            )
1870        })?;
1871
1872        // when saving a single buffer, we ignore whether or not it's dirty.
1873        if save_intent == SaveIntent::Save || save_intent == SaveIntent::SaveWithoutFormat {
1874            is_dirty = true;
1875        }
1876
1877        if save_intent == SaveIntent::SaveAs {
1878            is_dirty = true;
1879            has_conflict = false;
1880            can_save = false;
1881        }
1882
1883        if save_intent == SaveIntent::Overwrite {
1884            has_conflict = false;
1885        }
1886
1887        let should_format = save_intent != SaveIntent::SaveWithoutFormat;
1888
1889        if has_conflict && can_save {
1890            if has_deleted_file && is_singleton {
1891                let answer = pane.update_in(cx, |pane, window, cx| {
1892                    pane.activate_item(item_ix, true, true, window, cx);
1893                    window.prompt(
1894                        PromptLevel::Warning,
1895                        DELETED_MESSAGE,
1896                        None,
1897                        &["Save", "Close", "Cancel"],
1898                        cx,
1899                    )
1900                })?;
1901                match answer.await {
1902                    Ok(0) => {
1903                        pane.update_in(cx, |_, window, cx| {
1904                            item.save(
1905                                SaveOptions {
1906                                    format: should_format,
1907                                    autosave: false,
1908                                },
1909                                project,
1910                                window,
1911                                cx,
1912                            )
1913                        })?
1914                        .await?
1915                    }
1916                    Ok(1) => {
1917                        pane.update_in(cx, |pane, window, cx| {
1918                            pane.remove_item(item.item_id(), false, true, window, cx)
1919                        })?;
1920                    }
1921                    _ => return Ok(false),
1922                }
1923                return Ok(true);
1924            } else {
1925                let answer = pane.update_in(cx, |pane, window, cx| {
1926                    pane.activate_item(item_ix, true, true, window, cx);
1927                    window.prompt(
1928                        PromptLevel::Warning,
1929                        CONFLICT_MESSAGE,
1930                        None,
1931                        &["Overwrite", "Discard", "Cancel"],
1932                        cx,
1933                    )
1934                })?;
1935                match answer.await {
1936                    Ok(0) => {
1937                        pane.update_in(cx, |_, window, cx| {
1938                            item.save(
1939                                SaveOptions {
1940                                    format: should_format,
1941                                    autosave: false,
1942                                },
1943                                project,
1944                                window,
1945                                cx,
1946                            )
1947                        })?
1948                        .await?
1949                    }
1950                    Ok(1) => {
1951                        pane.update_in(cx, |_, window, cx| item.reload(project, window, cx))?
1952                            .await?
1953                    }
1954                    _ => return Ok(false),
1955                }
1956            }
1957        } else if is_dirty && (can_save || can_save_as) {
1958            if save_intent == SaveIntent::Close {
1959                let will_autosave = cx.update(|_window, cx| {
1960                    matches!(
1961                        item.workspace_settings(cx).autosave,
1962                        AutosaveSetting::OnFocusChange | AutosaveSetting::OnWindowChange
1963                    ) && item.can_autosave(cx)
1964                })?;
1965                if !will_autosave {
1966                    let item_id = item.item_id();
1967                    let answer_task = pane.update_in(cx, |pane, window, cx| {
1968                        if pane.save_modals_spawned.insert(item_id) {
1969                            pane.activate_item(item_ix, true, true, window, cx);
1970                            let prompt = dirty_message_for(item.project_path(cx));
1971                            Some(window.prompt(
1972                                PromptLevel::Warning,
1973                                &prompt,
1974                                None,
1975                                &["Save", "Don't Save", "Cancel"],
1976                                cx,
1977                            ))
1978                        } else {
1979                            None
1980                        }
1981                    })?;
1982                    if let Some(answer_task) = answer_task {
1983                        let answer = answer_task.await;
1984                        pane.update(cx, |pane, _| {
1985                            if !pane.save_modals_spawned.remove(&item_id) {
1986                                debug_panic!(
1987                                    "save modal was not present in spawned modals after awaiting for its answer"
1988                                )
1989                            }
1990                        })?;
1991                        match answer {
1992                            Ok(0) => {}
1993                            Ok(1) => {
1994                                // Don't save this file
1995                                pane.update_in(cx, |pane, window, cx| {
1996                                    if pane.is_tab_pinned(item_ix) && !item.can_save(cx) {
1997                                        pane.pinned_tab_count -= 1;
1998                                    }
1999                                    item.discarded(project, window, cx)
2000                                })
2001                                .log_err();
2002                                return Ok(true);
2003                            }
2004                            _ => return Ok(false), // Cancel
2005                        }
2006                    } else {
2007                        return Ok(false);
2008                    }
2009                }
2010            }
2011
2012            if can_save {
2013                pane.update_in(cx, |pane, window, cx| {
2014                    if pane.is_active_preview_item(item.item_id()) {
2015                        pane.set_preview_item_id(None, cx);
2016                    }
2017                    item.save(
2018                        SaveOptions {
2019                            format: should_format,
2020                            autosave: false,
2021                        },
2022                        project,
2023                        window,
2024                        cx,
2025                    )
2026                })?
2027                .await?;
2028            } else if can_save_as && is_singleton {
2029                let new_path = pane.update_in(cx, |pane, window, cx| {
2030                    pane.activate_item(item_ix, true, true, window, cx);
2031                    pane.workspace.update(cx, |workspace, cx| {
2032                        let lister = if workspace.project().read(cx).is_local() {
2033                            DirectoryLister::Local(
2034                                workspace.project().clone(),
2035                                workspace.app_state().fs.clone(),
2036                            )
2037                        } else {
2038                            DirectoryLister::Project(workspace.project().clone())
2039                        };
2040                        workspace.prompt_for_new_path(lister, window, cx)
2041                    })
2042                })??;
2043                let Some(new_path) = new_path.await.ok().flatten().into_iter().flatten().next()
2044                else {
2045                    return Ok(false);
2046                };
2047
2048                let project_path = pane
2049                    .update(cx, |pane, cx| {
2050                        pane.project
2051                            .update(cx, |project, cx| {
2052                                project.find_or_create_worktree(new_path, true, cx)
2053                            })
2054                            .ok()
2055                    })
2056                    .ok()
2057                    .flatten();
2058                let save_task = if let Some(project_path) = project_path {
2059                    let (worktree, path) = project_path.await?;
2060                    let worktree_id = worktree.read_with(cx, |worktree, _| worktree.id())?;
2061                    let new_path = ProjectPath {
2062                        worktree_id,
2063                        path: path.into(),
2064                    };
2065
2066                    pane.update_in(cx, |pane, window, cx| {
2067                        if let Some(item) = pane.item_for_path(new_path.clone(), cx) {
2068                            pane.remove_item(item.item_id(), false, false, window, cx);
2069                        }
2070
2071                        item.save_as(project, new_path, window, cx)
2072                    })?
2073                } else {
2074                    return Ok(false);
2075                };
2076
2077                save_task.await?;
2078                return Ok(true);
2079            }
2080        }
2081
2082        pane.update(cx, |_, cx| {
2083            cx.emit(Event::UserSavedItem {
2084                item: item.downgrade_item(),
2085                save_intent,
2086            });
2087            true
2088        })
2089    }
2090
2091    pub fn autosave_item(
2092        item: &dyn ItemHandle,
2093        project: Entity<Project>,
2094        window: &mut Window,
2095        cx: &mut App,
2096    ) -> Task<Result<()>> {
2097        let format = !matches!(
2098            item.workspace_settings(cx).autosave,
2099            AutosaveSetting::AfterDelay { .. }
2100        );
2101        if item.can_autosave(cx) {
2102            item.save(
2103                SaveOptions {
2104                    format,
2105                    autosave: true,
2106                },
2107                project,
2108                window,
2109                cx,
2110            )
2111        } else {
2112            Task::ready(Ok(()))
2113        }
2114    }
2115
2116    pub fn focus_active_item(&mut self, window: &mut Window, cx: &mut Context<Self>) {
2117        if let Some(active_item) = self.active_item() {
2118            let focus_handle = active_item.item_focus_handle(cx);
2119            window.focus(&focus_handle);
2120        }
2121    }
2122
2123    pub fn split(&mut self, direction: SplitDirection, cx: &mut Context<Self>) {
2124        cx.emit(Event::Split(direction));
2125    }
2126
2127    pub fn toolbar(&self) -> &Entity<Toolbar> {
2128        &self.toolbar
2129    }
2130
2131    pub fn handle_deleted_project_item(
2132        &mut self,
2133        entry_id: ProjectEntryId,
2134        window: &mut Window,
2135        cx: &mut Context<Pane>,
2136    ) -> Option<()> {
2137        let item_id = self.items().find_map(|item| {
2138            if item.is_singleton(cx) && item.project_entry_ids(cx).as_slice() == [entry_id] {
2139                Some(item.item_id())
2140            } else {
2141                None
2142            }
2143        })?;
2144
2145        self.remove_item(item_id, false, true, window, cx);
2146        self.nav_history.remove_item(item_id);
2147
2148        Some(())
2149    }
2150
2151    fn update_toolbar(&mut self, window: &mut Window, cx: &mut Context<Self>) {
2152        let active_item = self
2153            .items
2154            .get(self.active_item_index)
2155            .map(|item| item.as_ref());
2156        self.toolbar.update(cx, |toolbar, cx| {
2157            toolbar.set_active_item(active_item, window, cx);
2158        });
2159    }
2160
2161    fn update_status_bar(&mut self, window: &mut Window, cx: &mut Context<Self>) {
2162        let workspace = self.workspace.clone();
2163        let pane = cx.entity().clone();
2164
2165        window.defer(cx, move |window, cx| {
2166            let Ok(status_bar) =
2167                workspace.read_with(cx, |workspace, _| workspace.status_bar.clone())
2168            else {
2169                return;
2170            };
2171
2172            status_bar.update(cx, move |status_bar, cx| {
2173                status_bar.set_active_pane(&pane, window, cx);
2174            });
2175        });
2176    }
2177
2178    fn entry_abs_path(&self, entry: ProjectEntryId, cx: &App) -> Option<PathBuf> {
2179        let worktree = self
2180            .workspace
2181            .upgrade()?
2182            .read(cx)
2183            .project()
2184            .read(cx)
2185            .worktree_for_entry(entry, cx)?
2186            .read(cx);
2187        let entry = worktree.entry_for_id(entry)?;
2188        match &entry.canonical_path {
2189            Some(canonical_path) => Some(canonical_path.to_path_buf()),
2190            None => worktree.absolutize(&entry.path).ok(),
2191        }
2192    }
2193
2194    pub fn icon_color(selected: bool) -> Color {
2195        if selected {
2196            Color::Default
2197        } else {
2198            Color::Muted
2199        }
2200    }
2201
2202    fn toggle_pin_tab(&mut self, _: &TogglePinTab, window: &mut Window, cx: &mut Context<Self>) {
2203        if self.items.is_empty() {
2204            return;
2205        }
2206        let active_tab_ix = self.active_item_index();
2207        if self.is_tab_pinned(active_tab_ix) {
2208            self.unpin_tab_at(active_tab_ix, window, cx);
2209        } else {
2210            self.pin_tab_at(active_tab_ix, window, cx);
2211        }
2212    }
2213
2214    fn unpin_all_tabs(&mut self, _: &UnpinAllTabs, window: &mut Window, cx: &mut Context<Self>) {
2215        if self.items.is_empty() {
2216            return;
2217        }
2218
2219        let pinned_item_ids = self.pinned_item_ids().into_iter().rev();
2220
2221        for pinned_item_id in pinned_item_ids {
2222            if let Some(ix) = self.index_for_item_id(pinned_item_id) {
2223                self.unpin_tab_at(ix, window, cx);
2224            }
2225        }
2226    }
2227
2228    fn pin_tab_at(&mut self, ix: usize, window: &mut Window, cx: &mut Context<Self>) {
2229        self.change_tab_pin_state(ix, PinOperation::Pin, window, cx);
2230    }
2231
2232    fn unpin_tab_at(&mut self, ix: usize, window: &mut Window, cx: &mut Context<Self>) {
2233        self.change_tab_pin_state(ix, PinOperation::Unpin, window, cx);
2234    }
2235
2236    fn change_tab_pin_state(
2237        &mut self,
2238        ix: usize,
2239        operation: PinOperation,
2240        window: &mut Window,
2241        cx: &mut Context<Self>,
2242    ) {
2243        maybe!({
2244            let pane = cx.entity().clone();
2245
2246            let destination_index = match operation {
2247                PinOperation::Pin => self.pinned_tab_count.min(ix),
2248                PinOperation::Unpin => self.pinned_tab_count.checked_sub(1)?,
2249            };
2250
2251            let id = self.item_for_index(ix)?.item_id();
2252            let should_activate = ix == self.active_item_index;
2253
2254            if matches!(operation, PinOperation::Pin) && self.is_active_preview_item(id) {
2255                self.set_preview_item_id(None, cx);
2256            }
2257
2258            match operation {
2259                PinOperation::Pin => self.pinned_tab_count += 1,
2260                PinOperation::Unpin => self.pinned_tab_count -= 1,
2261            }
2262
2263            if ix == destination_index {
2264                cx.notify();
2265            } else {
2266                self.workspace
2267                    .update(cx, |_, cx| {
2268                        cx.defer_in(window, move |_, window, cx| {
2269                            move_item(
2270                                &pane,
2271                                &pane,
2272                                id,
2273                                destination_index,
2274                                should_activate,
2275                                window,
2276                                cx,
2277                            );
2278                        });
2279                    })
2280                    .ok()?;
2281            }
2282
2283            let event = match operation {
2284                PinOperation::Pin => Event::ItemPinned,
2285                PinOperation::Unpin => Event::ItemUnpinned,
2286            };
2287
2288            cx.emit(event);
2289
2290            Some(())
2291        });
2292    }
2293
2294    fn is_tab_pinned(&self, ix: usize) -> bool {
2295        self.pinned_tab_count > ix
2296    }
2297
2298    fn has_unpinned_tabs(&self) -> bool {
2299        self.pinned_tab_count < self.items.len()
2300    }
2301
2302    fn activate_unpinned_tab(&mut self, window: &mut Window, cx: &mut Context<Self>) {
2303        if self.items.is_empty() {
2304            return;
2305        }
2306        let Some(index) = self
2307            .items()
2308            .enumerate()
2309            .find_map(|(index, _item)| (!self.is_tab_pinned(index)).then_some(index))
2310        else {
2311            return;
2312        };
2313        self.activate_item(index, true, true, window, cx);
2314    }
2315
2316    fn render_tab(
2317        &self,
2318        ix: usize,
2319        item: &dyn ItemHandle,
2320        detail: usize,
2321        focus_handle: &FocusHandle,
2322        window: &mut Window,
2323        cx: &mut Context<Pane>,
2324    ) -> impl IntoElement + use<> {
2325        let is_active = ix == self.active_item_index;
2326        let is_preview = self
2327            .preview_item_id
2328            .map(|id| id == item.item_id())
2329            .unwrap_or(false);
2330
2331        let label = item.tab_content(
2332            TabContentParams {
2333                detail: Some(detail),
2334                selected: is_active,
2335                preview: is_preview,
2336                deemphasized: !self.has_focus(window, cx),
2337            },
2338            window,
2339            cx,
2340        );
2341
2342        let item_diagnostic = item
2343            .project_path(cx)
2344            .map_or(None, |project_path| self.diagnostics.get(&project_path));
2345
2346        let decorated_icon = item_diagnostic.map_or(None, |diagnostic| {
2347            let icon = match item.tab_icon(window, cx) {
2348                Some(icon) => icon,
2349                None => return None,
2350            };
2351
2352            let knockout_item_color = if is_active {
2353                cx.theme().colors().tab_active_background
2354            } else {
2355                cx.theme().colors().tab_bar_background
2356            };
2357
2358            let (icon_decoration, icon_color) = if matches!(diagnostic, &DiagnosticSeverity::ERROR)
2359            {
2360                (IconDecorationKind::X, Color::Error)
2361            } else {
2362                (IconDecorationKind::Triangle, Color::Warning)
2363            };
2364
2365            Some(DecoratedIcon::new(
2366                icon.size(IconSize::Small).color(Color::Muted),
2367                Some(
2368                    IconDecoration::new(icon_decoration, knockout_item_color, cx)
2369                        .color(icon_color.color(cx))
2370                        .position(Point {
2371                            x: px(-2.),
2372                            y: px(-2.),
2373                        }),
2374                ),
2375            ))
2376        });
2377
2378        let icon = if decorated_icon.is_none() {
2379            match item_diagnostic {
2380                Some(&DiagnosticSeverity::ERROR) => None,
2381                Some(&DiagnosticSeverity::WARNING) => None,
2382                _ => item
2383                    .tab_icon(window, cx)
2384                    .map(|icon| icon.color(Color::Muted)),
2385            }
2386            .map(|icon| icon.size(IconSize::Small))
2387        } else {
2388            None
2389        };
2390
2391        let settings = ItemSettings::get_global(cx);
2392        let close_side = &settings.close_position;
2393        let show_close_button = &settings.show_close_button;
2394        let indicator = render_item_indicator(item.boxed_clone(), cx);
2395        let item_id = item.item_id();
2396        let is_first_item = ix == 0;
2397        let is_last_item = ix == self.items.len() - 1;
2398        let is_pinned = self.is_tab_pinned(ix);
2399        let position_relative_to_active_item = ix.cmp(&self.active_item_index);
2400
2401        let tab = Tab::new(ix)
2402            .position(if is_first_item {
2403                TabPosition::First
2404            } else if is_last_item {
2405                TabPosition::Last
2406            } else {
2407                TabPosition::Middle(position_relative_to_active_item)
2408            })
2409            .close_side(match close_side {
2410                ClosePosition::Left => ui::TabCloseSide::Start,
2411                ClosePosition::Right => ui::TabCloseSide::End,
2412            })
2413            .toggle_state(is_active)
2414            .on_click(cx.listener(move |pane: &mut Self, _, window, cx| {
2415                pane.activate_item(ix, true, true, window, cx)
2416            }))
2417            // TODO: This should be a click listener with the middle mouse button instead of a mouse down listener.
2418            .on_mouse_down(
2419                MouseButton::Middle,
2420                cx.listener(move |pane, _event, window, cx| {
2421                    pane.close_item_by_id(item_id, SaveIntent::Close, window, cx)
2422                        .detach_and_log_err(cx);
2423                }),
2424            )
2425            .on_mouse_down(
2426                MouseButton::Left,
2427                cx.listener(move |pane, event: &MouseDownEvent, _, cx| {
2428                    if let Some(id) = pane.preview_item_id {
2429                        if id == item_id && event.click_count > 1 {
2430                            pane.set_preview_item_id(None, cx);
2431                        }
2432                    }
2433                }),
2434            )
2435            .on_drag(
2436                DraggedTab {
2437                    item: item.boxed_clone(),
2438                    pane: cx.entity().clone(),
2439                    detail,
2440                    is_active,
2441                    ix,
2442                },
2443                |tab, _, _, cx| cx.new(|_| tab.clone()),
2444            )
2445            .drag_over::<DraggedTab>(|tab, _, _, cx| {
2446                tab.bg(cx.theme().colors().drop_target_background)
2447            })
2448            .drag_over::<DraggedSelection>(|tab, _, _, cx| {
2449                tab.bg(cx.theme().colors().drop_target_background)
2450            })
2451            .when_some(self.can_drop_predicate.clone(), |this, p| {
2452                this.can_drop(move |a, window, cx| p(a, window, cx))
2453            })
2454            .on_drop(
2455                cx.listener(move |this, dragged_tab: &DraggedTab, window, cx| {
2456                    this.drag_split_direction = None;
2457                    this.handle_tab_drop(dragged_tab, ix, window, cx)
2458                }),
2459            )
2460            .on_drop(
2461                cx.listener(move |this, selection: &DraggedSelection, window, cx| {
2462                    this.drag_split_direction = None;
2463                    this.handle_dragged_selection_drop(selection, Some(ix), window, cx)
2464                }),
2465            )
2466            .on_drop(cx.listener(move |this, paths, window, cx| {
2467                this.drag_split_direction = None;
2468                this.handle_external_paths_drop(paths, window, cx)
2469            }))
2470            .when_some(item.tab_tooltip_content(cx), |tab, content| match content {
2471                TabTooltipContent::Text(text) => tab.tooltip(Tooltip::text(text.clone())),
2472                TabTooltipContent::Custom(element_fn) => {
2473                    tab.tooltip(move |window, cx| element_fn(window, cx))
2474                }
2475            })
2476            .start_slot::<Indicator>(indicator)
2477            .map(|this| {
2478                let end_slot_action: &'static dyn Action;
2479                let end_slot_tooltip_text: &'static str;
2480                let end_slot = if is_pinned {
2481                    end_slot_action = &TogglePinTab;
2482                    end_slot_tooltip_text = "Unpin Tab";
2483                    IconButton::new("unpin tab", IconName::Pin)
2484                        .shape(IconButtonShape::Square)
2485                        .icon_color(Color::Muted)
2486                        .size(ButtonSize::None)
2487                        .icon_size(IconSize::XSmall)
2488                        .on_click(cx.listener(move |pane, _, window, cx| {
2489                            pane.unpin_tab_at(ix, window, cx);
2490                        }))
2491                } else {
2492                    end_slot_action = &CloseActiveItem {
2493                        save_intent: None,
2494                        close_pinned: false,
2495                    };
2496                    end_slot_tooltip_text = "Close Tab";
2497                    match show_close_button {
2498                        ShowCloseButton::Always => IconButton::new("close tab", IconName::Close),
2499                        ShowCloseButton::Hover => {
2500                            IconButton::new("close tab", IconName::Close).visible_on_hover("")
2501                        }
2502                        ShowCloseButton::Hidden => return this,
2503                    }
2504                    .shape(IconButtonShape::Square)
2505                    .icon_color(Color::Muted)
2506                    .size(ButtonSize::None)
2507                    .icon_size(IconSize::XSmall)
2508                    .on_click(cx.listener(move |pane, _, window, cx| {
2509                        pane.close_item_by_id(item_id, SaveIntent::Close, window, cx)
2510                            .detach_and_log_err(cx);
2511                    }))
2512                }
2513                .map(|this| {
2514                    if is_active {
2515                        let focus_handle = focus_handle.clone();
2516                        this.tooltip(move |window, cx| {
2517                            Tooltip::for_action_in(
2518                                end_slot_tooltip_text,
2519                                end_slot_action,
2520                                &focus_handle,
2521                                window,
2522                                cx,
2523                            )
2524                        })
2525                    } else {
2526                        this.tooltip(Tooltip::text(end_slot_tooltip_text))
2527                    }
2528                });
2529                this.end_slot(end_slot)
2530            })
2531            .child(
2532                h_flex()
2533                    .gap_1()
2534                    .items_center()
2535                    .children(
2536                        std::iter::once(if let Some(decorated_icon) = decorated_icon {
2537                            Some(div().child(decorated_icon.into_any_element()))
2538                        } else if let Some(icon) = icon {
2539                            Some(div().child(icon.into_any_element()))
2540                        } else {
2541                            None
2542                        })
2543                        .flatten(),
2544                    )
2545                    .child(label),
2546            );
2547
2548        let single_entry_to_resolve = self.items[ix]
2549            .is_singleton(cx)
2550            .then(|| self.items[ix].project_entry_ids(cx).get(0).copied())
2551            .flatten();
2552
2553        let total_items = self.items.len();
2554        let has_items_to_left = ix > 0;
2555        let has_items_to_right = ix < total_items - 1;
2556        let has_clean_items = self.items.iter().any(|item| !item.is_dirty(cx));
2557        let is_pinned = self.is_tab_pinned(ix);
2558        let pane = cx.entity().downgrade();
2559        let menu_context = item.item_focus_handle(cx);
2560        right_click_menu(ix)
2561            .trigger(|_, _, _| tab)
2562            .menu(move |window, cx| {
2563                let pane = pane.clone();
2564                let menu_context = menu_context.clone();
2565                ContextMenu::build(window, cx, move |mut menu, window, cx| {
2566                    let close_active_item_action = CloseActiveItem {
2567                        save_intent: None,
2568                        close_pinned: true,
2569                    };
2570                    let close_inactive_items_action = CloseInactiveItems {
2571                        save_intent: None,
2572                        close_pinned: false,
2573                    };
2574                    let close_items_to_the_left_action = CloseItemsToTheLeft {
2575                        close_pinned: false,
2576                    };
2577                    let close_items_to_the_right_action = CloseItemsToTheRight {
2578                        close_pinned: false,
2579                    };
2580                    let close_clean_items_action = CloseCleanItems {
2581                        close_pinned: false,
2582                    };
2583                    let close_all_items_action = CloseAllItems {
2584                        save_intent: None,
2585                        close_pinned: false,
2586                    };
2587                    if let Some(pane) = pane.upgrade() {
2588                        menu = menu
2589                            .entry(
2590                                "Close",
2591                                Some(Box::new(close_active_item_action)),
2592                                window.handler_for(&pane, move |pane, window, cx| {
2593                                    pane.close_item_by_id(item_id, SaveIntent::Close, window, cx)
2594                                        .detach_and_log_err(cx);
2595                                }),
2596                            )
2597                            .item(ContextMenuItem::Entry(
2598                                ContextMenuEntry::new("Close Others")
2599                                    .action(Box::new(close_inactive_items_action.clone()))
2600                                    .disabled(total_items == 1)
2601                                    .handler(window.handler_for(&pane, move |pane, window, cx| {
2602                                        pane.close_inactive_items(
2603                                            &close_inactive_items_action,
2604                                            Some(item_id),
2605                                            window,
2606                                            cx,
2607                                        )
2608                                        .detach_and_log_err(cx);
2609                                    })),
2610                            ))
2611                            .separator()
2612                            .item(ContextMenuItem::Entry(
2613                                ContextMenuEntry::new("Close Left")
2614                                    .action(Box::new(close_items_to_the_left_action.clone()))
2615                                    .disabled(!has_items_to_left)
2616                                    .handler(window.handler_for(&pane, move |pane, window, cx| {
2617                                        pane.close_items_to_the_left_by_id(
2618                                            Some(item_id),
2619                                            &close_items_to_the_left_action,
2620                                            window,
2621                                            cx,
2622                                        )
2623                                        .detach_and_log_err(cx);
2624                                    })),
2625                            ))
2626                            .item(ContextMenuItem::Entry(
2627                                ContextMenuEntry::new("Close Right")
2628                                    .action(Box::new(close_items_to_the_right_action.clone()))
2629                                    .disabled(!has_items_to_right)
2630                                    .handler(window.handler_for(&pane, move |pane, window, cx| {
2631                                        pane.close_items_to_the_right_by_id(
2632                                            Some(item_id),
2633                                            &close_items_to_the_right_action,
2634                                            window,
2635                                            cx,
2636                                        )
2637                                        .detach_and_log_err(cx);
2638                                    })),
2639                            ))
2640                            .separator()
2641                            .item(ContextMenuItem::Entry(
2642                                ContextMenuEntry::new("Close Clean")
2643                                    .action(Box::new(close_clean_items_action.clone()))
2644                                    .disabled(!has_clean_items)
2645                                    .handler(window.handler_for(&pane, move |pane, window, cx| {
2646                                        pane.close_clean_items(
2647                                            &close_clean_items_action,
2648                                            window,
2649                                            cx,
2650                                        )
2651                                        .detach_and_log_err(cx)
2652                                    })),
2653                            ))
2654                            .entry(
2655                                "Close All",
2656                                Some(Box::new(close_all_items_action.clone())),
2657                                window.handler_for(&pane, move |pane, window, cx| {
2658                                    pane.close_all_items(&close_all_items_action, window, cx)
2659                                        .detach_and_log_err(cx)
2660                                }),
2661                            );
2662
2663                        let pin_tab_entries = |menu: ContextMenu| {
2664                            menu.separator().map(|this| {
2665                                if is_pinned {
2666                                    this.entry(
2667                                        "Unpin Tab",
2668                                        Some(TogglePinTab.boxed_clone()),
2669                                        window.handler_for(&pane, move |pane, window, cx| {
2670                                            pane.unpin_tab_at(ix, window, cx);
2671                                        }),
2672                                    )
2673                                } else {
2674                                    this.entry(
2675                                        "Pin Tab",
2676                                        Some(TogglePinTab.boxed_clone()),
2677                                        window.handler_for(&pane, move |pane, window, cx| {
2678                                            pane.pin_tab_at(ix, window, cx);
2679                                        }),
2680                                    )
2681                                }
2682                            })
2683                        };
2684                        if let Some(entry) = single_entry_to_resolve {
2685                            let project_path = pane
2686                                .read(cx)
2687                                .item_for_entry(entry, cx)
2688                                .and_then(|item| item.project_path(cx));
2689                            let worktree = project_path.as_ref().and_then(|project_path| {
2690                                pane.read(cx)
2691                                    .project
2692                                    .upgrade()?
2693                                    .read(cx)
2694                                    .worktree_for_id(project_path.worktree_id, cx)
2695                            });
2696                            let has_relative_path = worktree.as_ref().is_some_and(|worktree| {
2697                                worktree
2698                                    .read(cx)
2699                                    .root_entry()
2700                                    .map_or(false, |entry| entry.is_dir())
2701                            });
2702
2703                            let entry_abs_path = pane.read(cx).entry_abs_path(entry, cx);
2704                            let parent_abs_path = entry_abs_path
2705                                .as_deref()
2706                                .and_then(|abs_path| Some(abs_path.parent()?.to_path_buf()));
2707                            let relative_path = project_path
2708                                .map(|project_path| project_path.path)
2709                                .filter(|_| has_relative_path);
2710
2711                            let visible_in_project_panel = relative_path.is_some()
2712                                && worktree.is_some_and(|worktree| worktree.read(cx).is_visible());
2713
2714                            let entry_id = entry.to_proto();
2715                            menu = menu
2716                                .separator()
2717                                .when_some(entry_abs_path, |menu, abs_path| {
2718                                    menu.entry(
2719                                        "Copy Path",
2720                                        Some(Box::new(zed_actions::workspace::CopyPath)),
2721                                        window.handler_for(&pane, move |_, _, cx| {
2722                                            cx.write_to_clipboard(ClipboardItem::new_string(
2723                                                abs_path.to_string_lossy().to_string(),
2724                                            ));
2725                                        }),
2726                                    )
2727                                })
2728                                .when_some(relative_path, |menu, relative_path| {
2729                                    menu.entry(
2730                                        "Copy Relative Path",
2731                                        Some(Box::new(zed_actions::workspace::CopyRelativePath)),
2732                                        window.handler_for(&pane, move |_, _, cx| {
2733                                            cx.write_to_clipboard(ClipboardItem::new_string(
2734                                                relative_path.to_string_lossy().to_string(),
2735                                            ));
2736                                        }),
2737                                    )
2738                                })
2739                                .map(pin_tab_entries)
2740                                .separator()
2741                                .when(visible_in_project_panel, |menu| {
2742                                    menu.entry(
2743                                        "Reveal In Project Panel",
2744                                        Some(Box::new(RevealInProjectPanel::default())),
2745                                        window.handler_for(&pane, move |pane, _, cx| {
2746                                            pane.project
2747                                                .update(cx, |_, cx| {
2748                                                    cx.emit(project::Event::RevealInProjectPanel(
2749                                                        ProjectEntryId::from_proto(entry_id),
2750                                                    ))
2751                                                })
2752                                                .ok();
2753                                        }),
2754                                    )
2755                                })
2756                                .when_some(parent_abs_path, |menu, parent_abs_path| {
2757                                    menu.entry(
2758                                        "Open in Terminal",
2759                                        Some(Box::new(OpenInTerminal)),
2760                                        window.handler_for(&pane, move |_, window, cx| {
2761                                            window.dispatch_action(
2762                                                OpenTerminal {
2763                                                    working_directory: parent_abs_path.clone(),
2764                                                }
2765                                                .boxed_clone(),
2766                                                cx,
2767                                            );
2768                                        }),
2769                                    )
2770                                });
2771                        } else {
2772                            menu = menu.map(pin_tab_entries);
2773                        }
2774                    }
2775
2776                    menu.context(menu_context)
2777                })
2778            })
2779    }
2780
2781    fn render_tab_bar(&mut self, window: &mut Window, cx: &mut Context<Pane>) -> AnyElement {
2782        let focus_handle = self.focus_handle.clone();
2783        let navigate_backward = IconButton::new("navigate_backward", IconName::ArrowLeft)
2784            .icon_size(IconSize::Small)
2785            .on_click({
2786                let entity = cx.entity().clone();
2787                move |_, window, cx| {
2788                    entity.update(cx, |pane, cx| pane.navigate_backward(window, cx))
2789                }
2790            })
2791            .disabled(!self.can_navigate_backward())
2792            .tooltip({
2793                let focus_handle = focus_handle.clone();
2794                move |window, cx| {
2795                    Tooltip::for_action_in("Go Back", &GoBack, &focus_handle, window, cx)
2796                }
2797            });
2798
2799        let navigate_forward = IconButton::new("navigate_forward", IconName::ArrowRight)
2800            .icon_size(IconSize::Small)
2801            .on_click({
2802                let entity = cx.entity().clone();
2803                move |_, window, cx| entity.update(cx, |pane, cx| pane.navigate_forward(window, cx))
2804            })
2805            .disabled(!self.can_navigate_forward())
2806            .tooltip({
2807                let focus_handle = focus_handle.clone();
2808                move |window, cx| {
2809                    Tooltip::for_action_in("Go Forward", &GoForward, &focus_handle, window, cx)
2810                }
2811            });
2812
2813        let mut tab_items = self
2814            .items
2815            .iter()
2816            .enumerate()
2817            .zip(tab_details(&self.items, window, cx))
2818            .map(|((ix, item), detail)| {
2819                self.render_tab(ix, &**item, detail, &focus_handle, window, cx)
2820            })
2821            .collect::<Vec<_>>();
2822        let tab_count = tab_items.len();
2823        if self.pinned_tab_count > tab_count {
2824            log::warn!(
2825                "Pinned tab count ({}) exceeds actual tab count ({}). \
2826                This should not happen. If possible, add reproduction steps, \
2827                in a comment, to https://github.com/zed-industries/zed/issues/33342",
2828                self.pinned_tab_count,
2829                tab_count
2830            );
2831            self.pinned_tab_count = tab_count;
2832        }
2833        let unpinned_tabs = tab_items.split_off(self.pinned_tab_count);
2834        let pinned_tabs = tab_items;
2835        TabBar::new("tab_bar")
2836            .when(
2837                self.display_nav_history_buttons.unwrap_or_default(),
2838                |tab_bar| {
2839                    tab_bar
2840                        .start_child(navigate_backward)
2841                        .start_child(navigate_forward)
2842                },
2843            )
2844            .map(|tab_bar| {
2845                if self.show_tab_bar_buttons {
2846                    let render_tab_buttons = self.render_tab_bar_buttons.clone();
2847                    let (left_children, right_children) = render_tab_buttons(self, window, cx);
2848                    tab_bar
2849                        .start_children(left_children)
2850                        .end_children(right_children)
2851                } else {
2852                    tab_bar
2853                }
2854            })
2855            .children(pinned_tabs.len().ne(&0).then(|| {
2856                let content_width = self.tab_bar_scroll_handle.content_size().width;
2857                let viewport_width = self.tab_bar_scroll_handle.viewport().size.width;
2858                // We need to check both because offset returns delta values even when the scroll handle is not scrollable
2859                let is_scrollable = content_width > viewport_width;
2860                let is_scrolled = self.tab_bar_scroll_handle.offset().x < px(0.);
2861                let has_active_unpinned_tab = self.active_item_index >= self.pinned_tab_count;
2862                h_flex()
2863                    .children(pinned_tabs)
2864                    .when(is_scrollable && is_scrolled, |this| {
2865                        this.when(has_active_unpinned_tab, |this| this.border_r_2())
2866                            .when(!has_active_unpinned_tab, |this| this.border_r_1())
2867                            .border_color(cx.theme().colors().border)
2868                    })
2869            }))
2870            .child(
2871                h_flex()
2872                    .id("unpinned tabs")
2873                    .overflow_x_scroll()
2874                    .w_full()
2875                    .track_scroll(&self.tab_bar_scroll_handle)
2876                    .children(unpinned_tabs)
2877                    .child(
2878                        div()
2879                            .id("tab_bar_drop_target")
2880                            .min_w_6()
2881                            // HACK: This empty child is currently necessary to force the drop target to appear
2882                            // despite us setting a min width above.
2883                            .child("")
2884                            .h_full()
2885                            .flex_grow()
2886                            .drag_over::<DraggedTab>(|bar, _, _, cx| {
2887                                bar.bg(cx.theme().colors().drop_target_background)
2888                            })
2889                            .drag_over::<DraggedSelection>(|bar, _, _, cx| {
2890                                bar.bg(cx.theme().colors().drop_target_background)
2891                            })
2892                            .on_drop(cx.listener(
2893                                move |this, dragged_tab: &DraggedTab, window, cx| {
2894                                    this.drag_split_direction = None;
2895                                    this.handle_tab_drop(dragged_tab, this.items.len(), window, cx)
2896                                },
2897                            ))
2898                            .on_drop(cx.listener(
2899                                move |this, selection: &DraggedSelection, window, cx| {
2900                                    this.drag_split_direction = None;
2901                                    this.handle_project_entry_drop(
2902                                        &selection.active_selection.entry_id,
2903                                        Some(tab_count),
2904                                        window,
2905                                        cx,
2906                                    )
2907                                },
2908                            ))
2909                            .on_drop(cx.listener(move |this, paths, window, cx| {
2910                                this.drag_split_direction = None;
2911                                this.handle_external_paths_drop(paths, window, cx)
2912                            }))
2913                            .on_click(cx.listener(move |this, event: &ClickEvent, window, cx| {
2914                                if event.up.click_count == 2 {
2915                                    window.dispatch_action(
2916                                        this.double_click_dispatch_action.boxed_clone(),
2917                                        cx,
2918                                    );
2919                                }
2920                            })),
2921                    ),
2922            )
2923            .into_any_element()
2924    }
2925
2926    pub fn render_menu_overlay(menu: &Entity<ContextMenu>) -> Div {
2927        div().absolute().bottom_0().right_0().size_0().child(
2928            deferred(anchored().anchor(Corner::TopRight).child(menu.clone())).with_priority(1),
2929        )
2930    }
2931
2932    pub fn set_zoomed(&mut self, zoomed: bool, cx: &mut Context<Self>) {
2933        self.zoomed = zoomed;
2934        cx.notify();
2935    }
2936
2937    pub fn is_zoomed(&self) -> bool {
2938        self.zoomed
2939    }
2940
2941    fn handle_drag_move<T: 'static>(
2942        &mut self,
2943        event: &DragMoveEvent<T>,
2944        window: &mut Window,
2945        cx: &mut Context<Self>,
2946    ) {
2947        let can_split_predicate = self.can_split_predicate.take();
2948        let can_split = match &can_split_predicate {
2949            Some(can_split_predicate) => {
2950                can_split_predicate(self, event.dragged_item(), window, cx)
2951            }
2952            None => false,
2953        };
2954        self.can_split_predicate = can_split_predicate;
2955        if !can_split {
2956            return;
2957        }
2958
2959        let rect = event.bounds.size;
2960
2961        let size = event.bounds.size.width.min(event.bounds.size.height)
2962            * WorkspaceSettings::get_global(cx).drop_target_size;
2963
2964        let relative_cursor = Point::new(
2965            event.event.position.x - event.bounds.left(),
2966            event.event.position.y - event.bounds.top(),
2967        );
2968
2969        let direction = if relative_cursor.x < size
2970            || relative_cursor.x > rect.width - size
2971            || relative_cursor.y < size
2972            || relative_cursor.y > rect.height - size
2973        {
2974            [
2975                SplitDirection::Up,
2976                SplitDirection::Right,
2977                SplitDirection::Down,
2978                SplitDirection::Left,
2979            ]
2980            .iter()
2981            .min_by_key(|side| match side {
2982                SplitDirection::Up => relative_cursor.y,
2983                SplitDirection::Right => rect.width - relative_cursor.x,
2984                SplitDirection::Down => rect.height - relative_cursor.y,
2985                SplitDirection::Left => relative_cursor.x,
2986            })
2987            .cloned()
2988        } else {
2989            None
2990        };
2991
2992        if direction != self.drag_split_direction {
2993            self.drag_split_direction = direction;
2994        }
2995    }
2996
2997    pub fn handle_tab_drop(
2998        &mut self,
2999        dragged_tab: &DraggedTab,
3000        ix: usize,
3001        window: &mut Window,
3002        cx: &mut Context<Self>,
3003    ) {
3004        if let Some(custom_drop_handle) = self.custom_drop_handle.clone() {
3005            if let ControlFlow::Break(()) = custom_drop_handle(self, dragged_tab, window, cx) {
3006                return;
3007            }
3008        }
3009        let mut to_pane = cx.entity().clone();
3010        let split_direction = self.drag_split_direction;
3011        let item_id = dragged_tab.item.item_id();
3012        if let Some(preview_item_id) = self.preview_item_id {
3013            if item_id == preview_item_id {
3014                self.set_preview_item_id(None, cx);
3015            }
3016        }
3017
3018        let is_clone = cfg!(target_os = "macos") && window.modifiers().alt
3019            || cfg!(not(target_os = "macos")) && window.modifiers().control;
3020
3021        let from_pane = dragged_tab.pane.clone();
3022        let from_ix = dragged_tab.ix;
3023        self.workspace
3024            .update(cx, |_, cx| {
3025                cx.defer_in(window, move |workspace, window, cx| {
3026                    if let Some(split_direction) = split_direction {
3027                        to_pane = workspace.split_pane(to_pane, split_direction, window, cx);
3028                    }
3029                    let database_id = workspace.database_id();
3030                    let was_pinned_in_from_pane = from_pane.read_with(cx, |pane, _| {
3031                        pane.index_for_item_id(item_id)
3032                            .is_some_and(|ix| pane.is_tab_pinned(ix))
3033                    });
3034                    let to_pane_old_length = to_pane.read(cx).items.len();
3035                    if is_clone {
3036                        let Some(item) = from_pane
3037                            .read(cx)
3038                            .items()
3039                            .find(|item| item.item_id() == item_id)
3040                            .map(|item| item.clone())
3041                        else {
3042                            return;
3043                        };
3044                        if let Some(item) = item.clone_on_split(database_id, window, cx) {
3045                            to_pane.update(cx, |pane, cx| {
3046                                pane.add_item(item, true, true, None, window, cx);
3047                            })
3048                        }
3049                    } else {
3050                        move_item(&from_pane, &to_pane, item_id, ix, true, window, cx);
3051                    }
3052                    to_pane.update(cx, |this, _| {
3053                        if to_pane == from_pane {
3054                            let moved_right = ix > from_ix;
3055                            let ix = if moved_right { ix - 1 } else { ix };
3056                            let is_pinned_in_to_pane = this.is_tab_pinned(ix);
3057
3058                            if !was_pinned_in_from_pane && is_pinned_in_to_pane {
3059                                this.pinned_tab_count += 1;
3060                            } else if was_pinned_in_from_pane && !is_pinned_in_to_pane {
3061                                this.pinned_tab_count -= 1;
3062                            }
3063                        } else if this.items.len() >= to_pane_old_length {
3064                            let is_pinned_in_to_pane = this.is_tab_pinned(ix);
3065                            let item_created_pane = to_pane_old_length == 0;
3066                            let is_first_position = ix == 0;
3067                            let was_dropped_at_beginning = item_created_pane || is_first_position;
3068                            let should_remain_pinned = is_pinned_in_to_pane
3069                                || (was_pinned_in_from_pane && was_dropped_at_beginning);
3070
3071                            if should_remain_pinned {
3072                                this.pinned_tab_count += 1;
3073                            }
3074                        }
3075                    });
3076                });
3077            })
3078            .log_err();
3079    }
3080
3081    fn handle_dragged_selection_drop(
3082        &mut self,
3083        dragged_selection: &DraggedSelection,
3084        dragged_onto: Option<usize>,
3085        window: &mut Window,
3086        cx: &mut Context<Self>,
3087    ) {
3088        if let Some(custom_drop_handle) = self.custom_drop_handle.clone() {
3089            if let ControlFlow::Break(()) = custom_drop_handle(self, dragged_selection, window, cx)
3090            {
3091                return;
3092            }
3093        }
3094        self.handle_project_entry_drop(
3095            &dragged_selection.active_selection.entry_id,
3096            dragged_onto,
3097            window,
3098            cx,
3099        );
3100    }
3101
3102    fn handle_project_entry_drop(
3103        &mut self,
3104        project_entry_id: &ProjectEntryId,
3105        target: Option<usize>,
3106        window: &mut Window,
3107        cx: &mut Context<Self>,
3108    ) {
3109        if let Some(custom_drop_handle) = self.custom_drop_handle.clone() {
3110            if let ControlFlow::Break(()) = custom_drop_handle(self, project_entry_id, window, cx) {
3111                return;
3112            }
3113        }
3114        let mut to_pane = cx.entity().clone();
3115        let split_direction = self.drag_split_direction;
3116        let project_entry_id = *project_entry_id;
3117        self.workspace
3118            .update(cx, |_, cx| {
3119                cx.defer_in(window, move |workspace, window, cx| {
3120                    if let Some(project_path) = workspace
3121                        .project()
3122                        .read(cx)
3123                        .path_for_entry(project_entry_id, cx)
3124                    {
3125                        let load_path_task = workspace.load_path(project_path.clone(), window, cx);
3126                        cx.spawn_in(window, async move |workspace, cx| {
3127                            if let Some((project_entry_id, build_item)) =
3128                                load_path_task.await.notify_async_err(cx)
3129                            {
3130                                let (to_pane, new_item_handle) = workspace
3131                                    .update_in(cx, |workspace, window, cx| {
3132                                        if let Some(split_direction) = split_direction {
3133                                            to_pane = workspace.split_pane(
3134                                                to_pane,
3135                                                split_direction,
3136                                                window,
3137                                                cx,
3138                                            );
3139                                        }
3140                                        let new_item_handle = to_pane.update(cx, |pane, cx| {
3141                                            pane.open_item(
3142                                                project_entry_id,
3143                                                project_path,
3144                                                true,
3145                                                false,
3146                                                true,
3147                                                target,
3148                                                window,
3149                                                cx,
3150                                                build_item,
3151                                            )
3152                                        });
3153                                        (to_pane, new_item_handle)
3154                                    })
3155                                    .log_err()?;
3156                                to_pane
3157                                    .update_in(cx, |this, window, cx| {
3158                                        let Some(index) = this.index_for_item(&*new_item_handle)
3159                                        else {
3160                                            return;
3161                                        };
3162
3163                                        if target.map_or(false, |target| this.is_tab_pinned(target))
3164                                        {
3165                                            this.pin_tab_at(index, window, cx);
3166                                        }
3167                                    })
3168                                    .ok()?
3169                            }
3170                            Some(())
3171                        })
3172                        .detach();
3173                    };
3174                });
3175            })
3176            .log_err();
3177    }
3178
3179    fn handle_external_paths_drop(
3180        &mut self,
3181        paths: &ExternalPaths,
3182        window: &mut Window,
3183        cx: &mut Context<Self>,
3184    ) {
3185        if let Some(custom_drop_handle) = self.custom_drop_handle.clone() {
3186            if let ControlFlow::Break(()) = custom_drop_handle(self, paths, window, cx) {
3187                return;
3188            }
3189        }
3190        let mut to_pane = cx.entity().clone();
3191        let mut split_direction = self.drag_split_direction;
3192        let paths = paths.paths().to_vec();
3193        let is_remote = self
3194            .workspace
3195            .update(cx, |workspace, cx| {
3196                if workspace.project().read(cx).is_via_collab() {
3197                    workspace.show_error(
3198                        &anyhow::anyhow!("Cannot drop files on a remote project"),
3199                        cx,
3200                    );
3201                    true
3202                } else {
3203                    false
3204                }
3205            })
3206            .unwrap_or(true);
3207        if is_remote {
3208            return;
3209        }
3210
3211        self.workspace
3212            .update(cx, |workspace, cx| {
3213                let fs = Arc::clone(workspace.project().read(cx).fs());
3214                cx.spawn_in(window, async move |workspace, cx| {
3215                    let mut is_file_checks = FuturesUnordered::new();
3216                    for path in &paths {
3217                        is_file_checks.push(fs.is_file(path))
3218                    }
3219                    let mut has_files_to_open = false;
3220                    while let Some(is_file) = is_file_checks.next().await {
3221                        if is_file {
3222                            has_files_to_open = true;
3223                            break;
3224                        }
3225                    }
3226                    drop(is_file_checks);
3227                    if !has_files_to_open {
3228                        split_direction = None;
3229                    }
3230
3231                    if let Ok(open_task) = workspace.update_in(cx, |workspace, window, cx| {
3232                        if let Some(split_direction) = split_direction {
3233                            to_pane = workspace.split_pane(to_pane, split_direction, window, cx);
3234                        }
3235                        workspace.open_paths(
3236                            paths,
3237                            OpenOptions {
3238                                visible: Some(OpenVisible::OnlyDirectories),
3239                                ..Default::default()
3240                            },
3241                            Some(to_pane.downgrade()),
3242                            window,
3243                            cx,
3244                        )
3245                    }) {
3246                        let opened_items: Vec<_> = open_task.await;
3247                        _ = workspace.update(cx, |workspace, cx| {
3248                            for item in opened_items.into_iter().flatten() {
3249                                if let Err(e) = item {
3250                                    workspace.show_error(&e, cx);
3251                                }
3252                            }
3253                        });
3254                    }
3255                })
3256                .detach();
3257            })
3258            .log_err();
3259    }
3260
3261    pub fn display_nav_history_buttons(&mut self, display: Option<bool>) {
3262        self.display_nav_history_buttons = display;
3263    }
3264
3265    fn pinned_item_ids(&self) -> Vec<EntityId> {
3266        self.items
3267            .iter()
3268            .enumerate()
3269            .filter_map(|(index, item)| {
3270                if self.is_tab_pinned(index) {
3271                    return Some(item.item_id());
3272                }
3273
3274                None
3275            })
3276            .collect()
3277    }
3278
3279    fn clean_item_ids(&self, cx: &mut Context<Pane>) -> Vec<EntityId> {
3280        self.items()
3281            .filter_map(|item| {
3282                if !item.is_dirty(cx) {
3283                    return Some(item.item_id());
3284                }
3285
3286                None
3287            })
3288            .collect()
3289    }
3290
3291    fn to_the_side_item_ids(&self, item_id: EntityId, side: Side) -> Vec<EntityId> {
3292        match side {
3293            Side::Left => self
3294                .items()
3295                .take_while(|item| item.item_id() != item_id)
3296                .map(|item| item.item_id())
3297                .collect(),
3298            Side::Right => self
3299                .items()
3300                .rev()
3301                .take_while(|item| item.item_id() != item_id)
3302                .map(|item| item.item_id())
3303                .collect(),
3304        }
3305    }
3306
3307    pub fn drag_split_direction(&self) -> Option<SplitDirection> {
3308        self.drag_split_direction
3309    }
3310
3311    pub fn set_zoom_out_on_close(&mut self, zoom_out_on_close: bool) {
3312        self.zoom_out_on_close = zoom_out_on_close;
3313    }
3314}
3315
3316fn default_render_tab_bar_buttons(
3317    pane: &mut Pane,
3318    window: &mut Window,
3319    cx: &mut Context<Pane>,
3320) -> (Option<AnyElement>, Option<AnyElement>) {
3321    if !pane.has_focus(window, cx) && !pane.context_menu_focused(window, cx) {
3322        return (None, None);
3323    }
3324    // Ideally we would return a vec of elements here to pass directly to the [TabBar]'s
3325    // `end_slot`, but due to needing a view here that isn't possible.
3326    let right_children = h_flex()
3327        // Instead we need to replicate the spacing from the [TabBar]'s `end_slot` here.
3328        .gap(DynamicSpacing::Base04.rems(cx))
3329        .child(
3330            PopoverMenu::new("pane-tab-bar-popover-menu")
3331                .trigger_with_tooltip(
3332                    IconButton::new("plus", IconName::Plus).icon_size(IconSize::Small),
3333                    Tooltip::text("New..."),
3334                )
3335                .anchor(Corner::TopRight)
3336                .with_handle(pane.new_item_context_menu_handle.clone())
3337                .menu(move |window, cx| {
3338                    Some(ContextMenu::build(window, cx, |menu, _, _| {
3339                        menu.action("New File", NewFile.boxed_clone())
3340                            .action("Open File", ToggleFileFinder::default().boxed_clone())
3341                            .separator()
3342                            .action(
3343                                "Search Project",
3344                                DeploySearch {
3345                                    replace_enabled: false,
3346                                    included_files: None,
3347                                    excluded_files: None,
3348                                }
3349                                .boxed_clone(),
3350                            )
3351                            .action("Search Symbols", ToggleProjectSymbols.boxed_clone())
3352                            .separator()
3353                            .action("New Terminal", NewTerminal.boxed_clone())
3354                    }))
3355                }),
3356        )
3357        .child(
3358            PopoverMenu::new("pane-tab-bar-split")
3359                .trigger_with_tooltip(
3360                    IconButton::new("split", IconName::Split).icon_size(IconSize::Small),
3361                    Tooltip::text("Split Pane"),
3362                )
3363                .anchor(Corner::TopRight)
3364                .with_handle(pane.split_item_context_menu_handle.clone())
3365                .menu(move |window, cx| {
3366                    ContextMenu::build(window, cx, |menu, _, _| {
3367                        menu.action("Split Right", SplitRight.boxed_clone())
3368                            .action("Split Left", SplitLeft.boxed_clone())
3369                            .action("Split Up", SplitUp.boxed_clone())
3370                            .action("Split Down", SplitDown.boxed_clone())
3371                    })
3372                    .into()
3373                }),
3374        )
3375        .child({
3376            let zoomed = pane.is_zoomed();
3377            IconButton::new("toggle_zoom", IconName::Maximize)
3378                .icon_size(IconSize::Small)
3379                .toggle_state(zoomed)
3380                .selected_icon(IconName::Minimize)
3381                .on_click(cx.listener(|pane, _, window, cx| {
3382                    pane.toggle_zoom(&crate::ToggleZoom, window, cx);
3383                }))
3384                .tooltip(move |window, cx| {
3385                    Tooltip::for_action(
3386                        if zoomed { "Zoom Out" } else { "Zoom In" },
3387                        &ToggleZoom,
3388                        window,
3389                        cx,
3390                    )
3391                })
3392        })
3393        .into_any_element()
3394        .into();
3395    (None, right_children)
3396}
3397
3398impl Focusable for Pane {
3399    fn focus_handle(&self, _cx: &App) -> FocusHandle {
3400        self.focus_handle.clone()
3401    }
3402}
3403
3404impl Render for Pane {
3405    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
3406        let mut key_context = KeyContext::new_with_defaults();
3407        key_context.add("Pane");
3408        if self.active_item().is_none() {
3409            key_context.add("EmptyPane");
3410        }
3411
3412        let should_display_tab_bar = self.should_display_tab_bar.clone();
3413        let display_tab_bar = should_display_tab_bar(window, cx);
3414        let Some(project) = self.project.upgrade() else {
3415            return div().track_focus(&self.focus_handle(cx));
3416        };
3417        let is_local = project.read(cx).is_local();
3418
3419        v_flex()
3420            .key_context(key_context)
3421            .track_focus(&self.focus_handle(cx))
3422            .size_full()
3423            .flex_none()
3424            .overflow_hidden()
3425            .on_action(cx.listener(|pane, _: &AlternateFile, window, cx| {
3426                pane.alternate_file(window, cx);
3427            }))
3428            .on_action(
3429                cx.listener(|pane, _: &SplitLeft, _, cx| pane.split(SplitDirection::Left, cx)),
3430            )
3431            .on_action(cx.listener(|pane, _: &SplitUp, _, cx| pane.split(SplitDirection::Up, cx)))
3432            .on_action(cx.listener(|pane, _: &SplitHorizontal, _, cx| {
3433                pane.split(SplitDirection::horizontal(cx), cx)
3434            }))
3435            .on_action(cx.listener(|pane, _: &SplitVertical, _, cx| {
3436                pane.split(SplitDirection::vertical(cx), cx)
3437            }))
3438            .on_action(
3439                cx.listener(|pane, _: &SplitRight, _, cx| pane.split(SplitDirection::Right, cx)),
3440            )
3441            .on_action(
3442                cx.listener(|pane, _: &SplitDown, _, cx| pane.split(SplitDirection::Down, cx)),
3443            )
3444            .on_action(
3445                cx.listener(|pane, _: &GoBack, window, cx| pane.navigate_backward(window, cx)),
3446            )
3447            .on_action(
3448                cx.listener(|pane, _: &GoForward, window, cx| pane.navigate_forward(window, cx)),
3449            )
3450            .on_action(cx.listener(|_, _: &JoinIntoNext, _, cx| {
3451                cx.emit(Event::JoinIntoNext);
3452            }))
3453            .on_action(cx.listener(|_, _: &JoinAll, _, cx| {
3454                cx.emit(Event::JoinAll);
3455            }))
3456            .on_action(cx.listener(Pane::toggle_zoom))
3457            .on_action(
3458                cx.listener(|pane: &mut Pane, action: &ActivateItem, window, cx| {
3459                    pane.activate_item(
3460                        action.0.min(pane.items.len().saturating_sub(1)),
3461                        true,
3462                        true,
3463                        window,
3464                        cx,
3465                    );
3466                }),
3467            )
3468            .on_action(
3469                cx.listener(|pane: &mut Pane, _: &ActivateLastItem, window, cx| {
3470                    pane.activate_item(pane.items.len().saturating_sub(1), true, true, window, cx);
3471                }),
3472            )
3473            .on_action(
3474                cx.listener(|pane: &mut Pane, _: &ActivatePreviousItem, window, cx| {
3475                    pane.activate_prev_item(true, window, cx);
3476                }),
3477            )
3478            .on_action(
3479                cx.listener(|pane: &mut Pane, _: &ActivateNextItem, window, cx| {
3480                    pane.activate_next_item(true, window, cx);
3481                }),
3482            )
3483            .on_action(
3484                cx.listener(|pane, _: &SwapItemLeft, window, cx| pane.swap_item_left(window, cx)),
3485            )
3486            .on_action(
3487                cx.listener(|pane, _: &SwapItemRight, window, cx| pane.swap_item_right(window, cx)),
3488            )
3489            .on_action(cx.listener(|pane, action, window, cx| {
3490                pane.toggle_pin_tab(action, window, cx);
3491            }))
3492            .on_action(cx.listener(|pane, action, window, cx| {
3493                pane.unpin_all_tabs(action, window, cx);
3494            }))
3495            .when(PreviewTabsSettings::get_global(cx).enabled, |this| {
3496                this.on_action(cx.listener(|pane: &mut Pane, _: &TogglePreviewTab, _, cx| {
3497                    if let Some(active_item_id) = pane.active_item().map(|i| i.item_id()) {
3498                        if pane.is_active_preview_item(active_item_id) {
3499                            pane.set_preview_item_id(None, cx);
3500                        } else {
3501                            pane.set_preview_item_id(Some(active_item_id), cx);
3502                        }
3503                    }
3504                }))
3505            })
3506            .on_action(
3507                cx.listener(|pane: &mut Self, action: &CloseActiveItem, window, cx| {
3508                    pane.close_active_item(action, window, cx)
3509                        .detach_and_log_err(cx)
3510                }),
3511            )
3512            .on_action(
3513                cx.listener(|pane: &mut Self, action: &CloseInactiveItems, window, cx| {
3514                    pane.close_inactive_items(action, None, window, cx)
3515                        .detach_and_log_err(cx);
3516                }),
3517            )
3518            .on_action(
3519                cx.listener(|pane: &mut Self, action: &CloseCleanItems, window, cx| {
3520                    pane.close_clean_items(action, window, cx)
3521                        .detach_and_log_err(cx)
3522                }),
3523            )
3524            .on_action(cx.listener(
3525                |pane: &mut Self, action: &CloseItemsToTheLeft, window, cx| {
3526                    pane.close_items_to_the_left_by_id(None, action, window, cx)
3527                        .detach_and_log_err(cx)
3528                },
3529            ))
3530            .on_action(cx.listener(
3531                |pane: &mut Self, action: &CloseItemsToTheRight, window, cx| {
3532                    pane.close_items_to_the_right_by_id(None, action, window, cx)
3533                        .detach_and_log_err(cx)
3534                },
3535            ))
3536            .on_action(
3537                cx.listener(|pane: &mut Self, action: &CloseAllItems, window, cx| {
3538                    pane.close_all_items(action, window, cx)
3539                        .detach_and_log_err(cx)
3540                }),
3541            )
3542            .on_action(
3543                cx.listener(|pane: &mut Self, action: &RevealInProjectPanel, _, cx| {
3544                    let entry_id = action
3545                        .entry_id
3546                        .map(ProjectEntryId::from_proto)
3547                        .or_else(|| pane.active_item()?.project_entry_ids(cx).first().copied());
3548                    if let Some(entry_id) = entry_id {
3549                        pane.project
3550                            .update(cx, |_, cx| {
3551                                cx.emit(project::Event::RevealInProjectPanel(entry_id))
3552                            })
3553                            .ok();
3554                    }
3555                }),
3556            )
3557            .on_action(cx.listener(|_, _: &menu::Cancel, window, cx| {
3558                if cx.stop_active_drag(window) {
3559                    return;
3560                } else {
3561                    cx.propagate();
3562                }
3563            }))
3564            .when(self.active_item().is_some() && display_tab_bar, |pane| {
3565                pane.child((self.render_tab_bar.clone())(self, window, cx))
3566            })
3567            .child({
3568                let has_worktrees = project.read(cx).visible_worktrees(cx).next().is_some();
3569                // main content
3570                div()
3571                    .flex_1()
3572                    .relative()
3573                    .group("")
3574                    .overflow_hidden()
3575                    .on_drag_move::<DraggedTab>(cx.listener(Self::handle_drag_move))
3576                    .on_drag_move::<DraggedSelection>(cx.listener(Self::handle_drag_move))
3577                    .when(is_local, |div| {
3578                        div.on_drag_move::<ExternalPaths>(cx.listener(Self::handle_drag_move))
3579                    })
3580                    .map(|div| {
3581                        if let Some(item) = self.active_item() {
3582                            div.id("pane_placeholder")
3583                                .v_flex()
3584                                .size_full()
3585                                .overflow_hidden()
3586                                .child(self.toolbar.clone())
3587                                .child(item.to_any())
3588                        } else {
3589                            let placeholder = div
3590                                .id("pane_placeholder")
3591                                .h_flex()
3592                                .size_full()
3593                                .justify_center()
3594                                .on_click(cx.listener(
3595                                    move |this, event: &ClickEvent, window, cx| {
3596                                        if event.up.click_count == 2 {
3597                                            window.dispatch_action(
3598                                                this.double_click_dispatch_action.boxed_clone(),
3599                                                cx,
3600                                            );
3601                                        }
3602                                    },
3603                                ));
3604                            if has_worktrees {
3605                                placeholder
3606                            } else {
3607                                placeholder.child(
3608                                    Label::new("Open a file or project to get started.")
3609                                        .color(Color::Muted),
3610                                )
3611                            }
3612                        }
3613                    })
3614                    .child(
3615                        // drag target
3616                        div()
3617                            .invisible()
3618                            .absolute()
3619                            .bg(cx.theme().colors().drop_target_background)
3620                            .group_drag_over::<DraggedTab>("", |style| style.visible())
3621                            .group_drag_over::<DraggedSelection>("", |style| style.visible())
3622                            .when(is_local, |div| {
3623                                div.group_drag_over::<ExternalPaths>("", |style| style.visible())
3624                            })
3625                            .when_some(self.can_drop_predicate.clone(), |this, p| {
3626                                this.can_drop(move |a, window, cx| p(a, window, cx))
3627                            })
3628                            .on_drop(cx.listener(move |this, dragged_tab, window, cx| {
3629                                this.handle_tab_drop(
3630                                    dragged_tab,
3631                                    this.active_item_index(),
3632                                    window,
3633                                    cx,
3634                                )
3635                            }))
3636                            .on_drop(cx.listener(
3637                                move |this, selection: &DraggedSelection, window, cx| {
3638                                    this.handle_dragged_selection_drop(selection, None, window, cx)
3639                                },
3640                            ))
3641                            .on_drop(cx.listener(move |this, paths, window, cx| {
3642                                this.handle_external_paths_drop(paths, window, cx)
3643                            }))
3644                            .map(|div| {
3645                                let size = DefiniteLength::Fraction(0.5);
3646                                match self.drag_split_direction {
3647                                    None => div.top_0().right_0().bottom_0().left_0(),
3648                                    Some(SplitDirection::Up) => {
3649                                        div.top_0().left_0().right_0().h(size)
3650                                    }
3651                                    Some(SplitDirection::Down) => {
3652                                        div.left_0().bottom_0().right_0().h(size)
3653                                    }
3654                                    Some(SplitDirection::Left) => {
3655                                        div.top_0().left_0().bottom_0().w(size)
3656                                    }
3657                                    Some(SplitDirection::Right) => {
3658                                        div.top_0().bottom_0().right_0().w(size)
3659                                    }
3660                                }
3661                            }),
3662                    )
3663            })
3664            .on_mouse_down(
3665                MouseButton::Navigate(NavigationDirection::Back),
3666                cx.listener(|pane, _, window, cx| {
3667                    if let Some(workspace) = pane.workspace.upgrade() {
3668                        let pane = cx.entity().downgrade();
3669                        window.defer(cx, move |window, cx| {
3670                            workspace.update(cx, |workspace, cx| {
3671                                workspace.go_back(pane, window, cx).detach_and_log_err(cx)
3672                            })
3673                        })
3674                    }
3675                }),
3676            )
3677            .on_mouse_down(
3678                MouseButton::Navigate(NavigationDirection::Forward),
3679                cx.listener(|pane, _, window, cx| {
3680                    if let Some(workspace) = pane.workspace.upgrade() {
3681                        let pane = cx.entity().downgrade();
3682                        window.defer(cx, move |window, cx| {
3683                            workspace.update(cx, |workspace, cx| {
3684                                workspace
3685                                    .go_forward(pane, window, cx)
3686                                    .detach_and_log_err(cx)
3687                            })
3688                        })
3689                    }
3690                }),
3691            )
3692    }
3693}
3694
3695impl ItemNavHistory {
3696    pub fn push<D: 'static + Send + Any>(&mut self, data: Option<D>, cx: &mut App) {
3697        if self
3698            .item
3699            .upgrade()
3700            .is_some_and(|item| item.include_in_nav_history())
3701        {
3702            self.history
3703                .push(data, self.item.clone(), self.is_preview, cx);
3704        }
3705    }
3706
3707    pub fn pop_backward(&mut self, cx: &mut App) -> Option<NavigationEntry> {
3708        self.history.pop(NavigationMode::GoingBack, cx)
3709    }
3710
3711    pub fn pop_forward(&mut self, cx: &mut App) -> Option<NavigationEntry> {
3712        self.history.pop(NavigationMode::GoingForward, cx)
3713    }
3714}
3715
3716impl NavHistory {
3717    pub fn for_each_entry(
3718        &self,
3719        cx: &App,
3720        mut f: impl FnMut(&NavigationEntry, (ProjectPath, Option<PathBuf>)),
3721    ) {
3722        let borrowed_history = self.0.lock();
3723        borrowed_history
3724            .forward_stack
3725            .iter()
3726            .chain(borrowed_history.backward_stack.iter())
3727            .chain(borrowed_history.closed_stack.iter())
3728            .for_each(|entry| {
3729                if let Some(project_and_abs_path) =
3730                    borrowed_history.paths_by_item.get(&entry.item.id())
3731                {
3732                    f(entry, project_and_abs_path.clone());
3733                } else if let Some(item) = entry.item.upgrade() {
3734                    if let Some(path) = item.project_path(cx) {
3735                        f(entry, (path, None));
3736                    }
3737                }
3738            })
3739    }
3740
3741    pub fn set_mode(&mut self, mode: NavigationMode) {
3742        self.0.lock().mode = mode;
3743    }
3744
3745    pub fn mode(&self) -> NavigationMode {
3746        self.0.lock().mode
3747    }
3748
3749    pub fn disable(&mut self) {
3750        self.0.lock().mode = NavigationMode::Disabled;
3751    }
3752
3753    pub fn enable(&mut self) {
3754        self.0.lock().mode = NavigationMode::Normal;
3755    }
3756
3757    pub fn pop(&mut self, mode: NavigationMode, cx: &mut App) -> Option<NavigationEntry> {
3758        let mut state = self.0.lock();
3759        let entry = match mode {
3760            NavigationMode::Normal | NavigationMode::Disabled | NavigationMode::ClosingItem => {
3761                return None;
3762            }
3763            NavigationMode::GoingBack => &mut state.backward_stack,
3764            NavigationMode::GoingForward => &mut state.forward_stack,
3765            NavigationMode::ReopeningClosedItem => &mut state.closed_stack,
3766        }
3767        .pop_back();
3768        if entry.is_some() {
3769            state.did_update(cx);
3770        }
3771        entry
3772    }
3773
3774    pub fn push<D: 'static + Send + Any>(
3775        &mut self,
3776        data: Option<D>,
3777        item: Arc<dyn WeakItemHandle>,
3778        is_preview: bool,
3779        cx: &mut App,
3780    ) {
3781        let state = &mut *self.0.lock();
3782        match state.mode {
3783            NavigationMode::Disabled => {}
3784            NavigationMode::Normal | NavigationMode::ReopeningClosedItem => {
3785                if state.backward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
3786                    state.backward_stack.pop_front();
3787                }
3788                state.backward_stack.push_back(NavigationEntry {
3789                    item,
3790                    data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
3791                    timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
3792                    is_preview,
3793                });
3794                state.forward_stack.clear();
3795            }
3796            NavigationMode::GoingBack => {
3797                if state.forward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
3798                    state.forward_stack.pop_front();
3799                }
3800                state.forward_stack.push_back(NavigationEntry {
3801                    item,
3802                    data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
3803                    timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
3804                    is_preview,
3805                });
3806            }
3807            NavigationMode::GoingForward => {
3808                if state.backward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
3809                    state.backward_stack.pop_front();
3810                }
3811                state.backward_stack.push_back(NavigationEntry {
3812                    item,
3813                    data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
3814                    timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
3815                    is_preview,
3816                });
3817            }
3818            NavigationMode::ClosingItem => {
3819                if state.closed_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
3820                    state.closed_stack.pop_front();
3821                }
3822                state.closed_stack.push_back(NavigationEntry {
3823                    item,
3824                    data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
3825                    timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
3826                    is_preview,
3827                });
3828            }
3829        }
3830        state.did_update(cx);
3831    }
3832
3833    pub fn remove_item(&mut self, item_id: EntityId) {
3834        let mut state = self.0.lock();
3835        state.paths_by_item.remove(&item_id);
3836        state
3837            .backward_stack
3838            .retain(|entry| entry.item.id() != item_id);
3839        state
3840            .forward_stack
3841            .retain(|entry| entry.item.id() != item_id);
3842        state
3843            .closed_stack
3844            .retain(|entry| entry.item.id() != item_id);
3845    }
3846
3847    pub fn path_for_item(&self, item_id: EntityId) -> Option<(ProjectPath, Option<PathBuf>)> {
3848        self.0.lock().paths_by_item.get(&item_id).cloned()
3849    }
3850}
3851
3852impl NavHistoryState {
3853    pub fn did_update(&self, cx: &mut App) {
3854        if let Some(pane) = self.pane.upgrade() {
3855            cx.defer(move |cx| {
3856                pane.update(cx, |pane, cx| pane.history_updated(cx));
3857            });
3858        }
3859    }
3860}
3861
3862fn dirty_message_for(buffer_path: Option<ProjectPath>) -> String {
3863    let path = buffer_path
3864        .as_ref()
3865        .and_then(|p| {
3866            p.path
3867                .to_str()
3868                .and_then(|s| if s.is_empty() { None } else { Some(s) })
3869        })
3870        .unwrap_or("This buffer");
3871    let path = truncate_and_remove_front(path, 80);
3872    format!("{path} contains unsaved edits. Do you want to save it?")
3873}
3874
3875pub fn tab_details(items: &[Box<dyn ItemHandle>], _window: &Window, cx: &App) -> Vec<usize> {
3876    let mut tab_details = items.iter().map(|_| 0).collect::<Vec<_>>();
3877    let mut tab_descriptions = HashMap::default();
3878    let mut done = false;
3879    while !done {
3880        done = true;
3881
3882        // Store item indices by their tab description.
3883        for (ix, (item, detail)) in items.iter().zip(&tab_details).enumerate() {
3884            let description = item.tab_content_text(*detail, cx);
3885            if *detail == 0 || description != item.tab_content_text(detail - 1, cx) {
3886                tab_descriptions
3887                    .entry(description)
3888                    .or_insert(Vec::new())
3889                    .push(ix);
3890            }
3891        }
3892
3893        // If two or more items have the same tab description, increase their level
3894        // of detail and try again.
3895        for (_, item_ixs) in tab_descriptions.drain() {
3896            if item_ixs.len() > 1 {
3897                done = false;
3898                for ix in item_ixs {
3899                    tab_details[ix] += 1;
3900                }
3901            }
3902        }
3903    }
3904
3905    tab_details
3906}
3907
3908pub fn render_item_indicator(item: Box<dyn ItemHandle>, cx: &App) -> Option<Indicator> {
3909    maybe!({
3910        let indicator_color = match (item.has_conflict(cx), item.is_dirty(cx)) {
3911            (true, _) => Color::Warning,
3912            (_, true) => Color::Accent,
3913            (false, false) => return None,
3914        };
3915
3916        Some(Indicator::dot().color(indicator_color))
3917    })
3918}
3919
3920impl Render for DraggedTab {
3921    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
3922        let ui_font = ThemeSettings::get_global(cx).ui_font.clone();
3923        let label = self.item.tab_content(
3924            TabContentParams {
3925                detail: Some(self.detail),
3926                selected: false,
3927                preview: false,
3928                deemphasized: false,
3929            },
3930            window,
3931            cx,
3932        );
3933        Tab::new("")
3934            .toggle_state(self.is_active)
3935            .child(label)
3936            .render(window, cx)
3937            .font(ui_font)
3938    }
3939}
3940
3941#[cfg(test)]
3942mod tests {
3943    use std::num::NonZero;
3944
3945    use super::*;
3946    use crate::item::test::{TestItem, TestProjectItem};
3947    use gpui::{TestAppContext, VisualTestContext};
3948    use project::FakeFs;
3949    use settings::SettingsStore;
3950    use theme::LoadThemes;
3951    use util::TryFutureExt;
3952
3953    #[gpui::test]
3954    async fn test_add_item_capped_to_max_tabs(cx: &mut TestAppContext) {
3955        init_test(cx);
3956        let fs = FakeFs::new(cx.executor());
3957
3958        let project = Project::test(fs, None, cx).await;
3959        let (workspace, cx) =
3960            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
3961        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
3962
3963        for i in 0..7 {
3964            add_labeled_item(&pane, format!("{}", i).as_str(), false, cx);
3965        }
3966
3967        set_max_tabs(cx, Some(5));
3968        add_labeled_item(&pane, "7", false, cx);
3969        // Remove items to respect the max tab cap.
3970        assert_item_labels(&pane, ["3", "4", "5", "6", "7*"], cx);
3971        pane.update_in(cx, |pane, window, cx| {
3972            pane.activate_item(0, false, false, window, cx);
3973        });
3974        add_labeled_item(&pane, "X", false, cx);
3975        // Respect activation order.
3976        assert_item_labels(&pane, ["3", "X*", "5", "6", "7"], cx);
3977
3978        for i in 0..7 {
3979            add_labeled_item(&pane, format!("D{}", i).as_str(), true, cx);
3980        }
3981        // Keeps dirty items, even over max tab cap.
3982        assert_item_labels(
3983            &pane,
3984            ["D0^", "D1^", "D2^", "D3^", "D4^", "D5^", "D6*^"],
3985            cx,
3986        );
3987
3988        set_max_tabs(cx, None);
3989        for i in 0..7 {
3990            add_labeled_item(&pane, format!("N{}", i).as_str(), false, cx);
3991        }
3992        // No cap when max tabs is None.
3993        assert_item_labels(
3994            &pane,
3995            [
3996                "D0^", "D1^", "D2^", "D3^", "D4^", "D5^", "D6^", "N0", "N1", "N2", "N3", "N4",
3997                "N5", "N6*",
3998            ],
3999            cx,
4000        );
4001    }
4002
4003    #[gpui::test]
4004    async fn test_reduce_max_tabs_closes_existing_items(cx: &mut TestAppContext) {
4005        init_test(cx);
4006        let fs = FakeFs::new(cx.executor());
4007
4008        let project = Project::test(fs, None, cx).await;
4009        let (workspace, cx) =
4010            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4011        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4012
4013        add_labeled_item(&pane, "A", false, cx);
4014        add_labeled_item(&pane, "B", false, cx);
4015        let item_c = add_labeled_item(&pane, "C", false, cx);
4016        let item_d = add_labeled_item(&pane, "D", false, cx);
4017        add_labeled_item(&pane, "E", false, cx);
4018        add_labeled_item(&pane, "Settings", false, cx);
4019        assert_item_labels(&pane, ["A", "B", "C", "D", "E", "Settings*"], cx);
4020
4021        set_max_tabs(cx, Some(5));
4022        assert_item_labels(&pane, ["B", "C", "D", "E", "Settings*"], cx);
4023
4024        set_max_tabs(cx, Some(4));
4025        assert_item_labels(&pane, ["C", "D", "E", "Settings*"], cx);
4026
4027        pane.update_in(cx, |pane, window, cx| {
4028            let ix = pane.index_for_item_id(item_c.item_id()).unwrap();
4029            pane.pin_tab_at(ix, window, cx);
4030
4031            let ix = pane.index_for_item_id(item_d.item_id()).unwrap();
4032            pane.pin_tab_at(ix, window, cx);
4033        });
4034        assert_item_labels(&pane, ["C!", "D!", "E", "Settings*"], cx);
4035
4036        set_max_tabs(cx, Some(2));
4037        assert_item_labels(&pane, ["C!", "D!", "Settings*"], cx);
4038    }
4039
4040    #[gpui::test]
4041    async fn test_allow_pinning_dirty_item_at_max_tabs(cx: &mut TestAppContext) {
4042        init_test(cx);
4043        let fs = FakeFs::new(cx.executor());
4044
4045        let project = Project::test(fs, None, cx).await;
4046        let (workspace, cx) =
4047            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4048        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4049
4050        set_max_tabs(cx, Some(1));
4051        let item_a = add_labeled_item(&pane, "A", true, cx);
4052
4053        pane.update_in(cx, |pane, window, cx| {
4054            let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4055            pane.pin_tab_at(ix, window, cx);
4056        });
4057        assert_item_labels(&pane, ["A*^!"], cx);
4058    }
4059
4060    #[gpui::test]
4061    async fn test_allow_pinning_non_dirty_item_at_max_tabs(cx: &mut TestAppContext) {
4062        init_test(cx);
4063        let fs = FakeFs::new(cx.executor());
4064
4065        let project = Project::test(fs, None, cx).await;
4066        let (workspace, cx) =
4067            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4068        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4069
4070        set_max_tabs(cx, Some(1));
4071        let item_a = add_labeled_item(&pane, "A", false, cx);
4072
4073        pane.update_in(cx, |pane, window, cx| {
4074            let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4075            pane.pin_tab_at(ix, window, cx);
4076        });
4077        assert_item_labels(&pane, ["A*!"], cx);
4078    }
4079
4080    #[gpui::test]
4081    async fn test_pin_tabs_incrementally_at_max_capacity(cx: &mut TestAppContext) {
4082        init_test(cx);
4083        let fs = FakeFs::new(cx.executor());
4084
4085        let project = Project::test(fs, None, cx).await;
4086        let (workspace, cx) =
4087            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4088        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4089
4090        set_max_tabs(cx, Some(3));
4091
4092        let item_a = add_labeled_item(&pane, "A", false, cx);
4093        assert_item_labels(&pane, ["A*"], cx);
4094
4095        pane.update_in(cx, |pane, window, cx| {
4096            let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4097            pane.pin_tab_at(ix, window, cx);
4098        });
4099        assert_item_labels(&pane, ["A*!"], cx);
4100
4101        let item_b = add_labeled_item(&pane, "B", false, cx);
4102        assert_item_labels(&pane, ["A!", "B*"], cx);
4103
4104        pane.update_in(cx, |pane, window, cx| {
4105            let ix = pane.index_for_item_id(item_b.item_id()).unwrap();
4106            pane.pin_tab_at(ix, window, cx);
4107        });
4108        assert_item_labels(&pane, ["A!", "B*!"], cx);
4109
4110        let item_c = add_labeled_item(&pane, "C", false, cx);
4111        assert_item_labels(&pane, ["A!", "B!", "C*"], cx);
4112
4113        pane.update_in(cx, |pane, window, cx| {
4114            let ix = pane.index_for_item_id(item_c.item_id()).unwrap();
4115            pane.pin_tab_at(ix, window, cx);
4116        });
4117        assert_item_labels(&pane, ["A!", "B!", "C*!"], cx);
4118    }
4119
4120    #[gpui::test]
4121    async fn test_pin_tabs_left_to_right_after_opening_at_max_capacity(cx: &mut TestAppContext) {
4122        init_test(cx);
4123        let fs = FakeFs::new(cx.executor());
4124
4125        let project = Project::test(fs, None, cx).await;
4126        let (workspace, cx) =
4127            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4128        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4129
4130        set_max_tabs(cx, Some(3));
4131
4132        let item_a = add_labeled_item(&pane, "A", false, cx);
4133        assert_item_labels(&pane, ["A*"], cx);
4134
4135        let item_b = add_labeled_item(&pane, "B", false, cx);
4136        assert_item_labels(&pane, ["A", "B*"], cx);
4137
4138        let item_c = add_labeled_item(&pane, "C", false, cx);
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_a.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_b.item_id()).unwrap();
4149            pane.pin_tab_at(ix, window, cx);
4150        });
4151        assert_item_labels(&pane, ["A!", "B!", "C*"], cx);
4152
4153        pane.update_in(cx, |pane, window, cx| {
4154            let ix = pane.index_for_item_id(item_c.item_id()).unwrap();
4155            pane.pin_tab_at(ix, window, cx);
4156        });
4157        assert_item_labels(&pane, ["A!", "B!", "C*!"], cx);
4158    }
4159
4160    #[gpui::test]
4161    async fn test_pin_tabs_right_to_left_after_opening_at_max_capacity(cx: &mut TestAppContext) {
4162        init_test(cx);
4163        let fs = FakeFs::new(cx.executor());
4164
4165        let project = Project::test(fs, None, cx).await;
4166        let (workspace, cx) =
4167            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4168        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4169
4170        set_max_tabs(cx, Some(3));
4171
4172        let item_a = add_labeled_item(&pane, "A", false, cx);
4173        assert_item_labels(&pane, ["A*"], cx);
4174
4175        let item_b = add_labeled_item(&pane, "B", false, cx);
4176        assert_item_labels(&pane, ["A", "B*"], cx);
4177
4178        let item_c = add_labeled_item(&pane, "C", false, cx);
4179        assert_item_labels(&pane, ["A", "B", "C*"], cx);
4180
4181        pane.update_in(cx, |pane, window, cx| {
4182            let ix = pane.index_for_item_id(item_c.item_id()).unwrap();
4183            pane.pin_tab_at(ix, window, cx);
4184        });
4185        assert_item_labels(&pane, ["C*!", "A", "B"], cx);
4186
4187        pane.update_in(cx, |pane, window, cx| {
4188            let ix = pane.index_for_item_id(item_b.item_id()).unwrap();
4189            pane.pin_tab_at(ix, window, cx);
4190        });
4191        assert_item_labels(&pane, ["C*!", "B!", "A"], cx);
4192
4193        pane.update_in(cx, |pane, window, cx| {
4194            let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4195            pane.pin_tab_at(ix, window, cx);
4196        });
4197        assert_item_labels(&pane, ["C*!", "B!", "A!"], cx);
4198    }
4199
4200    #[gpui::test]
4201    async fn test_pinned_tabs_never_closed_at_max_tabs(cx: &mut TestAppContext) {
4202        init_test(cx);
4203        let fs = FakeFs::new(cx.executor());
4204
4205        let project = Project::test(fs, None, cx).await;
4206        let (workspace, cx) =
4207            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4208        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4209
4210        let item_a = add_labeled_item(&pane, "A", false, cx);
4211        pane.update_in(cx, |pane, window, cx| {
4212            let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4213            pane.pin_tab_at(ix, window, cx);
4214        });
4215
4216        let item_b = add_labeled_item(&pane, "B", false, cx);
4217        pane.update_in(cx, |pane, window, cx| {
4218            let ix = pane.index_for_item_id(item_b.item_id()).unwrap();
4219            pane.pin_tab_at(ix, window, cx);
4220        });
4221
4222        add_labeled_item(&pane, "C", false, cx);
4223        add_labeled_item(&pane, "D", false, cx);
4224        add_labeled_item(&pane, "E", false, cx);
4225        assert_item_labels(&pane, ["A!", "B!", "C", "D", "E*"], cx);
4226
4227        set_max_tabs(cx, Some(3));
4228        add_labeled_item(&pane, "F", false, cx);
4229        assert_item_labels(&pane, ["A!", "B!", "F*"], cx);
4230
4231        add_labeled_item(&pane, "G", false, cx);
4232        assert_item_labels(&pane, ["A!", "B!", "G*"], cx);
4233
4234        add_labeled_item(&pane, "H", false, cx);
4235        assert_item_labels(&pane, ["A!", "B!", "H*"], cx);
4236    }
4237
4238    #[gpui::test]
4239    async fn test_always_allows_one_unpinned_item_over_max_tabs_regardless_of_pinned_count(
4240        cx: &mut TestAppContext,
4241    ) {
4242        init_test(cx);
4243        let fs = FakeFs::new(cx.executor());
4244
4245        let project = Project::test(fs, None, cx).await;
4246        let (workspace, cx) =
4247            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4248        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4249
4250        set_max_tabs(cx, Some(3));
4251
4252        let item_a = add_labeled_item(&pane, "A", false, cx);
4253        pane.update_in(cx, |pane, window, cx| {
4254            let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4255            pane.pin_tab_at(ix, window, cx);
4256        });
4257
4258        let item_b = add_labeled_item(&pane, "B", false, cx);
4259        pane.update_in(cx, |pane, window, cx| {
4260            let ix = pane.index_for_item_id(item_b.item_id()).unwrap();
4261            pane.pin_tab_at(ix, window, cx);
4262        });
4263
4264        let item_c = add_labeled_item(&pane, "C", false, cx);
4265        pane.update_in(cx, |pane, window, cx| {
4266            let ix = pane.index_for_item_id(item_c.item_id()).unwrap();
4267            pane.pin_tab_at(ix, window, cx);
4268        });
4269
4270        assert_item_labels(&pane, ["A!", "B!", "C*!"], cx);
4271
4272        let item_d = add_labeled_item(&pane, "D", false, cx);
4273        assert_item_labels(&pane, ["A!", "B!", "C!", "D*"], cx);
4274
4275        pane.update_in(cx, |pane, window, cx| {
4276            let ix = pane.index_for_item_id(item_d.item_id()).unwrap();
4277            pane.pin_tab_at(ix, window, cx);
4278        });
4279        assert_item_labels(&pane, ["A!", "B!", "C!", "D*!"], cx);
4280
4281        add_labeled_item(&pane, "E", false, cx);
4282        assert_item_labels(&pane, ["A!", "B!", "C!", "D!", "E*"], cx);
4283
4284        add_labeled_item(&pane, "F", false, cx);
4285        assert_item_labels(&pane, ["A!", "B!", "C!", "D!", "F*"], cx);
4286    }
4287
4288    #[gpui::test]
4289    async fn test_can_open_one_item_when_all_tabs_are_dirty_at_max(cx: &mut TestAppContext) {
4290        init_test(cx);
4291        let fs = FakeFs::new(cx.executor());
4292
4293        let project = Project::test(fs, None, cx).await;
4294        let (workspace, cx) =
4295            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4296        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4297
4298        set_max_tabs(cx, Some(3));
4299
4300        add_labeled_item(&pane, "A", true, cx);
4301        assert_item_labels(&pane, ["A*^"], cx);
4302
4303        add_labeled_item(&pane, "B", true, cx);
4304        assert_item_labels(&pane, ["A^", "B*^"], cx);
4305
4306        add_labeled_item(&pane, "C", true, cx);
4307        assert_item_labels(&pane, ["A^", "B^", "C*^"], cx);
4308
4309        add_labeled_item(&pane, "D", false, cx);
4310        assert_item_labels(&pane, ["A^", "B^", "C^", "D*"], cx);
4311
4312        add_labeled_item(&pane, "E", false, cx);
4313        assert_item_labels(&pane, ["A^", "B^", "C^", "E*"], cx);
4314
4315        add_labeled_item(&pane, "F", false, cx);
4316        assert_item_labels(&pane, ["A^", "B^", "C^", "F*"], cx);
4317
4318        add_labeled_item(&pane, "G", true, cx);
4319        assert_item_labels(&pane, ["A^", "B^", "C^", "G*^"], cx);
4320    }
4321
4322    #[gpui::test]
4323    async fn test_toggle_pin_tab(cx: &mut TestAppContext) {
4324        init_test(cx);
4325        let fs = FakeFs::new(cx.executor());
4326
4327        let project = Project::test(fs, None, cx).await;
4328        let (workspace, cx) =
4329            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4330        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4331
4332        set_labeled_items(&pane, ["A", "B*", "C"], cx);
4333        assert_item_labels(&pane, ["A", "B*", "C"], cx);
4334
4335        pane.update_in(cx, |pane, window, cx| {
4336            pane.toggle_pin_tab(&TogglePinTab, window, cx);
4337        });
4338        assert_item_labels(&pane, ["B*!", "A", "C"], cx);
4339
4340        pane.update_in(cx, |pane, window, cx| {
4341            pane.toggle_pin_tab(&TogglePinTab, window, cx);
4342        });
4343        assert_item_labels(&pane, ["B*", "A", "C"], cx);
4344    }
4345
4346    #[gpui::test]
4347    async fn test_unpin_all_tabs(cx: &mut TestAppContext) {
4348        init_test(cx);
4349        let fs = FakeFs::new(cx.executor());
4350
4351        let project = Project::test(fs, None, cx).await;
4352        let (workspace, cx) =
4353            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4354        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4355
4356        // Unpin all, in an empty pane
4357        pane.update_in(cx, |pane, window, cx| {
4358            pane.unpin_all_tabs(&UnpinAllTabs, window, cx);
4359        });
4360
4361        assert_item_labels(&pane, [], cx);
4362
4363        let item_a = add_labeled_item(&pane, "A", false, cx);
4364        let item_b = add_labeled_item(&pane, "B", false, cx);
4365        let item_c = add_labeled_item(&pane, "C", false, cx);
4366        assert_item_labels(&pane, ["A", "B", "C*"], cx);
4367
4368        // Unpin all, when no tabs are pinned
4369        pane.update_in(cx, |pane, window, cx| {
4370            pane.unpin_all_tabs(&UnpinAllTabs, window, cx);
4371        });
4372
4373        assert_item_labels(&pane, ["A", "B", "C*"], cx);
4374
4375        // Pin inactive tabs only
4376        pane.update_in(cx, |pane, window, cx| {
4377            let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4378            pane.pin_tab_at(ix, window, cx);
4379
4380            let ix = pane.index_for_item_id(item_b.item_id()).unwrap();
4381            pane.pin_tab_at(ix, window, cx);
4382        });
4383        assert_item_labels(&pane, ["A!", "B!", "C*"], cx);
4384
4385        pane.update_in(cx, |pane, window, cx| {
4386            pane.unpin_all_tabs(&UnpinAllTabs, window, cx);
4387        });
4388
4389        assert_item_labels(&pane, ["A", "B", "C*"], cx);
4390
4391        // Pin all tabs
4392        pane.update_in(cx, |pane, window, cx| {
4393            let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4394            pane.pin_tab_at(ix, window, cx);
4395
4396            let ix = pane.index_for_item_id(item_b.item_id()).unwrap();
4397            pane.pin_tab_at(ix, window, cx);
4398
4399            let ix = pane.index_for_item_id(item_c.item_id()).unwrap();
4400            pane.pin_tab_at(ix, window, cx);
4401        });
4402        assert_item_labels(&pane, ["A!", "B!", "C*!"], cx);
4403
4404        // Activate middle tab
4405        pane.update_in(cx, |pane, window, cx| {
4406            pane.activate_item(1, false, false, window, cx);
4407        });
4408        assert_item_labels(&pane, ["A!", "B*!", "C!"], cx);
4409
4410        pane.update_in(cx, |pane, window, cx| {
4411            pane.unpin_all_tabs(&UnpinAllTabs, window, cx);
4412        });
4413
4414        // Order has not changed
4415        assert_item_labels(&pane, ["A", "B*", "C"], cx);
4416    }
4417
4418    #[gpui::test]
4419    async fn test_pinning_active_tab_without_position_change_maintains_focus(
4420        cx: &mut TestAppContext,
4421    ) {
4422        init_test(cx);
4423        let fs = FakeFs::new(cx.executor());
4424
4425        let project = Project::test(fs, None, cx).await;
4426        let (workspace, cx) =
4427            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4428        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4429
4430        // Add A
4431        let item_a = add_labeled_item(&pane, "A", false, cx);
4432        assert_item_labels(&pane, ["A*"], cx);
4433
4434        // Add B
4435        add_labeled_item(&pane, "B", false, cx);
4436        assert_item_labels(&pane, ["A", "B*"], cx);
4437
4438        // Activate A again
4439        pane.update_in(cx, |pane, window, cx| {
4440            let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4441            pane.activate_item(ix, true, true, window, cx);
4442        });
4443        assert_item_labels(&pane, ["A*", "B"], cx);
4444
4445        // Pin A - remains active
4446        pane.update_in(cx, |pane, window, cx| {
4447            let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4448            pane.pin_tab_at(ix, window, cx);
4449        });
4450        assert_item_labels(&pane, ["A*!", "B"], cx);
4451
4452        // Unpin A - remain active
4453        pane.update_in(cx, |pane, window, cx| {
4454            let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4455            pane.unpin_tab_at(ix, window, cx);
4456        });
4457        assert_item_labels(&pane, ["A*", "B"], cx);
4458    }
4459
4460    #[gpui::test]
4461    async fn test_pinning_active_tab_with_position_change_maintains_focus(cx: &mut TestAppContext) {
4462        init_test(cx);
4463        let fs = FakeFs::new(cx.executor());
4464
4465        let project = Project::test(fs, None, cx).await;
4466        let (workspace, cx) =
4467            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4468        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4469
4470        // Add A, B, C
4471        add_labeled_item(&pane, "A", false, cx);
4472        add_labeled_item(&pane, "B", false, cx);
4473        let item_c = add_labeled_item(&pane, "C", false, cx);
4474        assert_item_labels(&pane, ["A", "B", "C*"], cx);
4475
4476        // Pin C - moves to pinned area, remains active
4477        pane.update_in(cx, |pane, window, cx| {
4478            let ix = pane.index_for_item_id(item_c.item_id()).unwrap();
4479            pane.pin_tab_at(ix, window, cx);
4480        });
4481        assert_item_labels(&pane, ["C*!", "A", "B"], cx);
4482
4483        // Unpin C - moves after pinned area, remains active
4484        pane.update_in(cx, |pane, window, cx| {
4485            let ix = pane.index_for_item_id(item_c.item_id()).unwrap();
4486            pane.unpin_tab_at(ix, window, cx);
4487        });
4488        assert_item_labels(&pane, ["C*", "A", "B"], cx);
4489    }
4490
4491    #[gpui::test]
4492    async fn test_pinning_inactive_tab_without_position_change_preserves_existing_focus(
4493        cx: &mut TestAppContext,
4494    ) {
4495        init_test(cx);
4496        let fs = FakeFs::new(cx.executor());
4497
4498        let project = Project::test(fs, None, cx).await;
4499        let (workspace, cx) =
4500            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4501        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4502
4503        // Add A, B
4504        let item_a = add_labeled_item(&pane, "A", false, cx);
4505        add_labeled_item(&pane, "B", false, cx);
4506        assert_item_labels(&pane, ["A", "B*"], cx);
4507
4508        // Pin A - already in pinned area, B remains active
4509        pane.update_in(cx, |pane, window, cx| {
4510            let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4511            pane.pin_tab_at(ix, window, cx);
4512        });
4513        assert_item_labels(&pane, ["A!", "B*"], cx);
4514
4515        // Unpin A - stays in place, B remains active
4516        pane.update_in(cx, |pane, window, cx| {
4517            let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4518            pane.unpin_tab_at(ix, window, cx);
4519        });
4520        assert_item_labels(&pane, ["A", "B*"], cx);
4521    }
4522
4523    #[gpui::test]
4524    async fn test_pinning_inactive_tab_with_position_change_preserves_existing_focus(
4525        cx: &mut TestAppContext,
4526    ) {
4527        init_test(cx);
4528        let fs = FakeFs::new(cx.executor());
4529
4530        let project = Project::test(fs, None, cx).await;
4531        let (workspace, cx) =
4532            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4533        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4534
4535        // Add A, B, C
4536        add_labeled_item(&pane, "A", false, cx);
4537        let item_b = add_labeled_item(&pane, "B", false, cx);
4538        let item_c = add_labeled_item(&pane, "C", false, cx);
4539        assert_item_labels(&pane, ["A", "B", "C*"], cx);
4540
4541        // Activate B
4542        pane.update_in(cx, |pane, window, cx| {
4543            let ix = pane.index_for_item_id(item_b.item_id()).unwrap();
4544            pane.activate_item(ix, true, true, window, cx);
4545        });
4546        assert_item_labels(&pane, ["A", "B*", "C"], cx);
4547
4548        // Pin C - moves to pinned area, B remains active
4549        pane.update_in(cx, |pane, window, cx| {
4550            let ix = pane.index_for_item_id(item_c.item_id()).unwrap();
4551            pane.pin_tab_at(ix, window, cx);
4552        });
4553        assert_item_labels(&pane, ["C!", "A", "B*"], cx);
4554
4555        // Unpin C - moves after pinned area, B remains active
4556        pane.update_in(cx, |pane, window, cx| {
4557            let ix = pane.index_for_item_id(item_c.item_id()).unwrap();
4558            pane.unpin_tab_at(ix, window, cx);
4559        });
4560        assert_item_labels(&pane, ["C", "A", "B*"], cx);
4561    }
4562
4563    #[gpui::test]
4564    async fn test_drag_unpinned_tab_to_split_creates_pane_with_unpinned_tab(
4565        cx: &mut TestAppContext,
4566    ) {
4567        init_test(cx);
4568        let fs = FakeFs::new(cx.executor());
4569
4570        let project = Project::test(fs, None, cx).await;
4571        let (workspace, cx) =
4572            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4573        let pane_a = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4574
4575        // Add A, B. Pin B. Activate A
4576        let item_a = add_labeled_item(&pane_a, "A", false, cx);
4577        let item_b = add_labeled_item(&pane_a, "B", false, cx);
4578
4579        pane_a.update_in(cx, |pane, window, cx| {
4580            let ix = pane.index_for_item_id(item_b.item_id()).unwrap();
4581            pane.pin_tab_at(ix, window, cx);
4582
4583            let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4584            pane.activate_item(ix, true, true, window, cx);
4585        });
4586
4587        // Drag A to create new split
4588        pane_a.update_in(cx, |pane, window, cx| {
4589            pane.drag_split_direction = Some(SplitDirection::Right);
4590
4591            let dragged_tab = DraggedTab {
4592                pane: pane_a.clone(),
4593                item: item_a.boxed_clone(),
4594                ix: 0,
4595                detail: 0,
4596                is_active: true,
4597            };
4598            pane.handle_tab_drop(&dragged_tab, 0, window, cx);
4599        });
4600
4601        // A should be moved to new pane. B should remain pinned, A should not be pinned
4602        let (pane_a, pane_b) = workspace.read_with(cx, |workspace, _| {
4603            let panes = workspace.panes();
4604            (panes[0].clone(), panes[1].clone())
4605        });
4606        assert_item_labels(&pane_a, ["B*!"], cx);
4607        assert_item_labels(&pane_b, ["A*"], cx);
4608    }
4609
4610    #[gpui::test]
4611    async fn test_drag_pinned_tab_to_split_creates_pane_with_pinned_tab(cx: &mut TestAppContext) {
4612        init_test(cx);
4613        let fs = FakeFs::new(cx.executor());
4614
4615        let project = Project::test(fs, None, cx).await;
4616        let (workspace, cx) =
4617            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4618        let pane_a = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4619
4620        // Add A, B. Pin both. Activate A
4621        let item_a = add_labeled_item(&pane_a, "A", false, cx);
4622        let item_b = add_labeled_item(&pane_a, "B", false, cx);
4623
4624        pane_a.update_in(cx, |pane, window, cx| {
4625            let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4626            pane.pin_tab_at(ix, window, cx);
4627
4628            let ix = pane.index_for_item_id(item_b.item_id()).unwrap();
4629            pane.pin_tab_at(ix, window, cx);
4630
4631            let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4632            pane.activate_item(ix, true, true, window, cx);
4633        });
4634        assert_item_labels(&pane_a, ["A*!", "B!"], cx);
4635
4636        // Drag A to create new split
4637        pane_a.update_in(cx, |pane, window, cx| {
4638            pane.drag_split_direction = Some(SplitDirection::Right);
4639
4640            let dragged_tab = DraggedTab {
4641                pane: pane_a.clone(),
4642                item: item_a.boxed_clone(),
4643                ix: 0,
4644                detail: 0,
4645                is_active: true,
4646            };
4647            pane.handle_tab_drop(&dragged_tab, 0, window, cx);
4648        });
4649
4650        // A should be moved to new pane. Both A and B should still be pinned
4651        let (pane_a, pane_b) = workspace.read_with(cx, |workspace, _| {
4652            let panes = workspace.panes();
4653            (panes[0].clone(), panes[1].clone())
4654        });
4655        assert_item_labels(&pane_a, ["B*!"], cx);
4656        assert_item_labels(&pane_b, ["A*!"], cx);
4657    }
4658
4659    #[gpui::test]
4660    async fn test_drag_pinned_tab_into_existing_panes_pinned_region(cx: &mut TestAppContext) {
4661        init_test(cx);
4662        let fs = FakeFs::new(cx.executor());
4663
4664        let project = Project::test(fs, None, cx).await;
4665        let (workspace, cx) =
4666            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4667        let pane_a = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4668
4669        // Add A to pane A and pin
4670        let item_a = add_labeled_item(&pane_a, "A", false, cx);
4671        pane_a.update_in(cx, |pane, window, cx| {
4672            let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4673            pane.pin_tab_at(ix, window, cx);
4674        });
4675        assert_item_labels(&pane_a, ["A*!"], cx);
4676
4677        // Add B to pane B and pin
4678        let pane_b = workspace.update_in(cx, |workspace, window, cx| {
4679            workspace.split_pane(pane_a.clone(), SplitDirection::Right, window, cx)
4680        });
4681        let item_b = add_labeled_item(&pane_b, "B", false, cx);
4682        pane_b.update_in(cx, |pane, window, cx| {
4683            let ix = pane.index_for_item_id(item_b.item_id()).unwrap();
4684            pane.pin_tab_at(ix, window, cx);
4685        });
4686        assert_item_labels(&pane_b, ["B*!"], cx);
4687
4688        // Move A from pane A to pane B's pinned region
4689        pane_b.update_in(cx, |pane, window, cx| {
4690            let dragged_tab = DraggedTab {
4691                pane: pane_a.clone(),
4692                item: item_a.boxed_clone(),
4693                ix: 0,
4694                detail: 0,
4695                is_active: true,
4696            };
4697            pane.handle_tab_drop(&dragged_tab, 0, window, cx);
4698        });
4699
4700        // A should stay pinned
4701        assert_item_labels(&pane_a, [], cx);
4702        assert_item_labels(&pane_b, ["A*!", "B!"], cx);
4703    }
4704
4705    #[gpui::test]
4706    async fn test_drag_pinned_tab_into_existing_panes_unpinned_region(cx: &mut TestAppContext) {
4707        init_test(cx);
4708        let fs = FakeFs::new(cx.executor());
4709
4710        let project = Project::test(fs, None, cx).await;
4711        let (workspace, cx) =
4712            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4713        let pane_a = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4714
4715        // Add A to pane A and pin
4716        let item_a = add_labeled_item(&pane_a, "A", false, cx);
4717        pane_a.update_in(cx, |pane, window, cx| {
4718            let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4719            pane.pin_tab_at(ix, window, cx);
4720        });
4721        assert_item_labels(&pane_a, ["A*!"], cx);
4722
4723        // Create pane B with pinned item B
4724        let pane_b = workspace.update_in(cx, |workspace, window, cx| {
4725            workspace.split_pane(pane_a.clone(), SplitDirection::Right, window, cx)
4726        });
4727        let item_b = add_labeled_item(&pane_b, "B", false, cx);
4728        assert_item_labels(&pane_b, ["B*"], cx);
4729
4730        pane_b.update_in(cx, |pane, window, cx| {
4731            let ix = pane.index_for_item_id(item_b.item_id()).unwrap();
4732            pane.pin_tab_at(ix, window, cx);
4733        });
4734        assert_item_labels(&pane_b, ["B*!"], cx);
4735
4736        // Move A from pane A to pane B's unpinned region
4737        pane_b.update_in(cx, |pane, window, cx| {
4738            let dragged_tab = DraggedTab {
4739                pane: pane_a.clone(),
4740                item: item_a.boxed_clone(),
4741                ix: 0,
4742                detail: 0,
4743                is_active: true,
4744            };
4745            pane.handle_tab_drop(&dragged_tab, 1, window, cx);
4746        });
4747
4748        // A should become pinned
4749        assert_item_labels(&pane_a, [], cx);
4750        assert_item_labels(&pane_b, ["B!", "A*"], cx);
4751    }
4752
4753    #[gpui::test]
4754    async fn test_drag_pinned_tab_into_existing_panes_first_position_with_no_pinned_tabs(
4755        cx: &mut TestAppContext,
4756    ) {
4757        init_test(cx);
4758        let fs = FakeFs::new(cx.executor());
4759
4760        let project = Project::test(fs, None, cx).await;
4761        let (workspace, cx) =
4762            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4763        let pane_a = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4764
4765        // Add A to pane A and pin
4766        let item_a = add_labeled_item(&pane_a, "A", false, cx);
4767        pane_a.update_in(cx, |pane, window, cx| {
4768            let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4769            pane.pin_tab_at(ix, window, cx);
4770        });
4771        assert_item_labels(&pane_a, ["A*!"], cx);
4772
4773        // Add B to pane B
4774        let pane_b = workspace.update_in(cx, |workspace, window, cx| {
4775            workspace.split_pane(pane_a.clone(), SplitDirection::Right, window, cx)
4776        });
4777        add_labeled_item(&pane_b, "B", false, cx);
4778        assert_item_labels(&pane_b, ["B*"], cx);
4779
4780        // Move A from pane A to position 0 in pane B, indicating it should stay pinned
4781        pane_b.update_in(cx, |pane, window, cx| {
4782            let dragged_tab = DraggedTab {
4783                pane: pane_a.clone(),
4784                item: item_a.boxed_clone(),
4785                ix: 0,
4786                detail: 0,
4787                is_active: true,
4788            };
4789            pane.handle_tab_drop(&dragged_tab, 0, window, cx);
4790        });
4791
4792        // A should stay pinned
4793        assert_item_labels(&pane_a, [], cx);
4794        assert_item_labels(&pane_b, ["A*!", "B"], cx);
4795    }
4796
4797    #[gpui::test]
4798    async fn test_drag_pinned_tab_into_existing_pane_at_max_capacity_closes_unpinned_tabs(
4799        cx: &mut TestAppContext,
4800    ) {
4801        init_test(cx);
4802        let fs = FakeFs::new(cx.executor());
4803
4804        let project = Project::test(fs, None, cx).await;
4805        let (workspace, cx) =
4806            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4807        let pane_a = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4808        set_max_tabs(cx, Some(2));
4809
4810        // Add A, B to pane A. Pin both
4811        let item_a = add_labeled_item(&pane_a, "A", false, cx);
4812        let item_b = add_labeled_item(&pane_a, "B", false, cx);
4813        pane_a.update_in(cx, |pane, window, cx| {
4814            let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4815            pane.pin_tab_at(ix, window, cx);
4816
4817            let ix = pane.index_for_item_id(item_b.item_id()).unwrap();
4818            pane.pin_tab_at(ix, window, cx);
4819        });
4820        assert_item_labels(&pane_a, ["A!", "B*!"], cx);
4821
4822        // Add C, D to pane B. Pin both
4823        let pane_b = workspace.update_in(cx, |workspace, window, cx| {
4824            workspace.split_pane(pane_a.clone(), SplitDirection::Right, window, cx)
4825        });
4826        let item_c = add_labeled_item(&pane_b, "C", false, cx);
4827        let item_d = add_labeled_item(&pane_b, "D", false, cx);
4828        pane_b.update_in(cx, |pane, window, cx| {
4829            let ix = pane.index_for_item_id(item_c.item_id()).unwrap();
4830            pane.pin_tab_at(ix, window, cx);
4831
4832            let ix = pane.index_for_item_id(item_d.item_id()).unwrap();
4833            pane.pin_tab_at(ix, window, cx);
4834        });
4835        assert_item_labels(&pane_b, ["C!", "D*!"], cx);
4836
4837        // Add a third unpinned item to pane B (exceeds max tabs), but is allowed,
4838        // as we allow 1 tab over max if the others are pinned or dirty
4839        add_labeled_item(&pane_b, "E", false, cx);
4840        assert_item_labels(&pane_b, ["C!", "D!", "E*"], cx);
4841
4842        // Drag pinned A from pane A to position 0 in pane B
4843        pane_b.update_in(cx, |pane, window, cx| {
4844            let dragged_tab = DraggedTab {
4845                pane: pane_a.clone(),
4846                item: item_a.boxed_clone(),
4847                ix: 0,
4848                detail: 0,
4849                is_active: true,
4850            };
4851            pane.handle_tab_drop(&dragged_tab, 0, window, cx);
4852        });
4853
4854        // E (unpinned) should be closed, leaving 3 pinned items
4855        assert_item_labels(&pane_a, ["B*!"], cx);
4856        assert_item_labels(&pane_b, ["A*!", "C!", "D!"], cx);
4857    }
4858
4859    #[gpui::test]
4860    async fn test_drag_last_pinned_tab_to_same_position_stays_pinned(cx: &mut TestAppContext) {
4861        init_test(cx);
4862        let fs = FakeFs::new(cx.executor());
4863
4864        let project = Project::test(fs, None, cx).await;
4865        let (workspace, cx) =
4866            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4867        let pane_a = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4868
4869        // Add A to pane A and pin it
4870        let item_a = add_labeled_item(&pane_a, "A", false, cx);
4871        pane_a.update_in(cx, |pane, window, cx| {
4872            let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4873            pane.pin_tab_at(ix, window, cx);
4874        });
4875        assert_item_labels(&pane_a, ["A*!"], cx);
4876
4877        // Drag pinned A to position 1 (directly to the right) in the same pane
4878        pane_a.update_in(cx, |pane, window, cx| {
4879            let dragged_tab = DraggedTab {
4880                pane: pane_a.clone(),
4881                item: item_a.boxed_clone(),
4882                ix: 0,
4883                detail: 0,
4884                is_active: true,
4885            };
4886            pane.handle_tab_drop(&dragged_tab, 1, window, cx);
4887        });
4888
4889        // A should still be pinned and active
4890        assert_item_labels(&pane_a, ["A*!"], cx);
4891    }
4892
4893    #[gpui::test]
4894    async fn test_drag_pinned_tab_beyond_last_pinned_tab_in_same_pane_stays_pinned(
4895        cx: &mut TestAppContext,
4896    ) {
4897        init_test(cx);
4898        let fs = FakeFs::new(cx.executor());
4899
4900        let project = Project::test(fs, None, cx).await;
4901        let (workspace, cx) =
4902            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4903        let pane_a = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4904
4905        // Add A, B to pane A and pin both
4906        let item_a = add_labeled_item(&pane_a, "A", false, cx);
4907        let item_b = add_labeled_item(&pane_a, "B", false, cx);
4908        pane_a.update_in(cx, |pane, window, cx| {
4909            let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4910            pane.pin_tab_at(ix, window, cx);
4911
4912            let ix = pane.index_for_item_id(item_b.item_id()).unwrap();
4913            pane.pin_tab_at(ix, window, cx);
4914        });
4915        assert_item_labels(&pane_a, ["A!", "B*!"], cx);
4916
4917        // Drag pinned A right of B in the same pane
4918        pane_a.update_in(cx, |pane, window, cx| {
4919            let dragged_tab = DraggedTab {
4920                pane: pane_a.clone(),
4921                item: item_a.boxed_clone(),
4922                ix: 0,
4923                detail: 0,
4924                is_active: true,
4925            };
4926            pane.handle_tab_drop(&dragged_tab, 2, window, cx);
4927        });
4928
4929        // A stays pinned
4930        assert_item_labels(&pane_a, ["B!", "A*!"], cx);
4931    }
4932
4933    #[gpui::test]
4934    async fn test_drag_pinned_tab_beyond_unpinned_tab_in_same_pane_becomes_unpinned(
4935        cx: &mut TestAppContext,
4936    ) {
4937        init_test(cx);
4938        let fs = FakeFs::new(cx.executor());
4939
4940        let project = Project::test(fs, None, cx).await;
4941        let (workspace, cx) =
4942            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4943        let pane_a = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4944
4945        // Add A, B to pane A and pin A
4946        let item_a = add_labeled_item(&pane_a, "A", false, cx);
4947        add_labeled_item(&pane_a, "B", false, cx);
4948        pane_a.update_in(cx, |pane, window, cx| {
4949            let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4950            pane.pin_tab_at(ix, window, cx);
4951        });
4952        assert_item_labels(&pane_a, ["A!", "B*"], cx);
4953
4954        // Drag pinned A right of B in the same pane
4955        pane_a.update_in(cx, |pane, window, cx| {
4956            let dragged_tab = DraggedTab {
4957                pane: pane_a.clone(),
4958                item: item_a.boxed_clone(),
4959                ix: 0,
4960                detail: 0,
4961                is_active: true,
4962            };
4963            pane.handle_tab_drop(&dragged_tab, 2, window, cx);
4964        });
4965
4966        // A becomes unpinned
4967        assert_item_labels(&pane_a, ["B", "A*"], cx);
4968    }
4969
4970    #[gpui::test]
4971    async fn test_drag_unpinned_tab_in_front_of_pinned_tab_in_same_pane_becomes_pinned(
4972        cx: &mut TestAppContext,
4973    ) {
4974        init_test(cx);
4975        let fs = FakeFs::new(cx.executor());
4976
4977        let project = Project::test(fs, None, cx).await;
4978        let (workspace, cx) =
4979            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4980        let pane_a = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4981
4982        // Add A, B to pane A and pin A
4983        let item_a = add_labeled_item(&pane_a, "A", false, cx);
4984        let item_b = add_labeled_item(&pane_a, "B", false, cx);
4985        pane_a.update_in(cx, |pane, window, cx| {
4986            let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4987            pane.pin_tab_at(ix, window, cx);
4988        });
4989        assert_item_labels(&pane_a, ["A!", "B*"], cx);
4990
4991        // Drag pinned B left of A in the same pane
4992        pane_a.update_in(cx, |pane, window, cx| {
4993            let dragged_tab = DraggedTab {
4994                pane: pane_a.clone(),
4995                item: item_b.boxed_clone(),
4996                ix: 1,
4997                detail: 0,
4998                is_active: true,
4999            };
5000            pane.handle_tab_drop(&dragged_tab, 0, window, cx);
5001        });
5002
5003        // A becomes unpinned
5004        assert_item_labels(&pane_a, ["B*!", "A!"], cx);
5005    }
5006
5007    #[gpui::test]
5008    async fn test_drag_unpinned_tab_to_the_pinned_region_stays_pinned(cx: &mut TestAppContext) {
5009        init_test(cx);
5010        let fs = FakeFs::new(cx.executor());
5011
5012        let project = Project::test(fs, None, cx).await;
5013        let (workspace, cx) =
5014            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
5015        let pane_a = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
5016
5017        // Add A, B, C to pane A and pin A
5018        let item_a = add_labeled_item(&pane_a, "A", false, cx);
5019        add_labeled_item(&pane_a, "B", false, cx);
5020        let item_c = add_labeled_item(&pane_a, "C", false, cx);
5021        pane_a.update_in(cx, |pane, window, cx| {
5022            let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
5023            pane.pin_tab_at(ix, window, cx);
5024        });
5025        assert_item_labels(&pane_a, ["A!", "B", "C*"], cx);
5026
5027        // Drag pinned C left of B in the same pane
5028        pane_a.update_in(cx, |pane, window, cx| {
5029            let dragged_tab = DraggedTab {
5030                pane: pane_a.clone(),
5031                item: item_c.boxed_clone(),
5032                ix: 2,
5033                detail: 0,
5034                is_active: true,
5035            };
5036            pane.handle_tab_drop(&dragged_tab, 1, window, cx);
5037        });
5038
5039        // A stays pinned, B and C remain unpinned
5040        assert_item_labels(&pane_a, ["A!", "C*", "B"], cx);
5041    }
5042
5043    #[gpui::test]
5044    async fn test_drag_unpinned_tab_into_existing_panes_pinned_region(cx: &mut TestAppContext) {
5045        init_test(cx);
5046        let fs = FakeFs::new(cx.executor());
5047
5048        let project = Project::test(fs, None, cx).await;
5049        let (workspace, cx) =
5050            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
5051        let pane_a = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
5052
5053        // Add unpinned item A to pane A
5054        let item_a = add_labeled_item(&pane_a, "A", false, cx);
5055        assert_item_labels(&pane_a, ["A*"], cx);
5056
5057        // Create pane B with pinned item B
5058        let pane_b = workspace.update_in(cx, |workspace, window, cx| {
5059            workspace.split_pane(pane_a.clone(), SplitDirection::Right, window, cx)
5060        });
5061        let item_b = add_labeled_item(&pane_b, "B", false, cx);
5062        pane_b.update_in(cx, |pane, window, cx| {
5063            let ix = pane.index_for_item_id(item_b.item_id()).unwrap();
5064            pane.pin_tab_at(ix, window, cx);
5065        });
5066        assert_item_labels(&pane_b, ["B*!"], cx);
5067
5068        // Move A from pane A to pane B's pinned region
5069        pane_b.update_in(cx, |pane, window, cx| {
5070            let dragged_tab = DraggedTab {
5071                pane: pane_a.clone(),
5072                item: item_a.boxed_clone(),
5073                ix: 0,
5074                detail: 0,
5075                is_active: true,
5076            };
5077            pane.handle_tab_drop(&dragged_tab, 0, window, cx);
5078        });
5079
5080        // A should become pinned since it was dropped in the pinned region
5081        assert_item_labels(&pane_a, [], cx);
5082        assert_item_labels(&pane_b, ["A*!", "B!"], cx);
5083    }
5084
5085    #[gpui::test]
5086    async fn test_drag_unpinned_tab_into_existing_panes_unpinned_region(cx: &mut TestAppContext) {
5087        init_test(cx);
5088        let fs = FakeFs::new(cx.executor());
5089
5090        let project = Project::test(fs, None, cx).await;
5091        let (workspace, cx) =
5092            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
5093        let pane_a = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
5094
5095        // Add unpinned item A to pane A
5096        let item_a = add_labeled_item(&pane_a, "A", false, cx);
5097        assert_item_labels(&pane_a, ["A*"], cx);
5098
5099        // Create pane B with one pinned item B
5100        let pane_b = workspace.update_in(cx, |workspace, window, cx| {
5101            workspace.split_pane(pane_a.clone(), SplitDirection::Right, window, cx)
5102        });
5103        let item_b = add_labeled_item(&pane_b, "B", false, cx);
5104        pane_b.update_in(cx, |pane, window, cx| {
5105            let ix = pane.index_for_item_id(item_b.item_id()).unwrap();
5106            pane.pin_tab_at(ix, window, cx);
5107        });
5108        assert_item_labels(&pane_b, ["B*!"], cx);
5109
5110        // Move A from pane A to pane B's unpinned region
5111        pane_b.update_in(cx, |pane, window, cx| {
5112            let dragged_tab = DraggedTab {
5113                pane: pane_a.clone(),
5114                item: item_a.boxed_clone(),
5115                ix: 0,
5116                detail: 0,
5117                is_active: true,
5118            };
5119            pane.handle_tab_drop(&dragged_tab, 1, window, cx);
5120        });
5121
5122        // A should remain unpinned since it was dropped outside the pinned region
5123        assert_item_labels(&pane_a, [], cx);
5124        assert_item_labels(&pane_b, ["B!", "A*"], cx);
5125    }
5126
5127    #[gpui::test]
5128    async fn test_drag_pinned_tab_throughout_entire_range_of_pinned_tabs_both_directions(
5129        cx: &mut TestAppContext,
5130    ) {
5131        init_test(cx);
5132        let fs = FakeFs::new(cx.executor());
5133
5134        let project = Project::test(fs, None, cx).await;
5135        let (workspace, cx) =
5136            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
5137        let pane_a = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
5138
5139        // Add A, B, C and pin all
5140        let item_a = add_labeled_item(&pane_a, "A", false, cx);
5141        let item_b = add_labeled_item(&pane_a, "B", false, cx);
5142        let item_c = add_labeled_item(&pane_a, "C", false, cx);
5143        assert_item_labels(&pane_a, ["A", "B", "C*"], cx);
5144
5145        pane_a.update_in(cx, |pane, window, cx| {
5146            let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
5147            pane.pin_tab_at(ix, window, cx);
5148
5149            let ix = pane.index_for_item_id(item_b.item_id()).unwrap();
5150            pane.pin_tab_at(ix, window, cx);
5151
5152            let ix = pane.index_for_item_id(item_c.item_id()).unwrap();
5153            pane.pin_tab_at(ix, window, cx);
5154        });
5155        assert_item_labels(&pane_a, ["A!", "B!", "C*!"], cx);
5156
5157        // Move A to right of B
5158        pane_a.update_in(cx, |pane, window, cx| {
5159            let dragged_tab = DraggedTab {
5160                pane: pane_a.clone(),
5161                item: item_a.boxed_clone(),
5162                ix: 0,
5163                detail: 0,
5164                is_active: true,
5165            };
5166            pane.handle_tab_drop(&dragged_tab, 1, window, cx);
5167        });
5168
5169        // A should be after B and all are pinned
5170        assert_item_labels(&pane_a, ["B!", "A*!", "C!"], cx);
5171
5172        // Move A to right of C
5173        pane_a.update_in(cx, |pane, window, cx| {
5174            let dragged_tab = DraggedTab {
5175                pane: pane_a.clone(),
5176                item: item_a.boxed_clone(),
5177                ix: 1,
5178                detail: 0,
5179                is_active: true,
5180            };
5181            pane.handle_tab_drop(&dragged_tab, 2, window, cx);
5182        });
5183
5184        // A should be after C and all are pinned
5185        assert_item_labels(&pane_a, ["B!", "C!", "A*!"], cx);
5186
5187        // Move A to left of C
5188        pane_a.update_in(cx, |pane, window, cx| {
5189            let dragged_tab = DraggedTab {
5190                pane: pane_a.clone(),
5191                item: item_a.boxed_clone(),
5192                ix: 2,
5193                detail: 0,
5194                is_active: true,
5195            };
5196            pane.handle_tab_drop(&dragged_tab, 1, window, cx);
5197        });
5198
5199        // A should be before C and all are pinned
5200        assert_item_labels(&pane_a, ["B!", "A*!", "C!"], cx);
5201
5202        // Move A to left of B
5203        pane_a.update_in(cx, |pane, window, cx| {
5204            let dragged_tab = DraggedTab {
5205                pane: pane_a.clone(),
5206                item: item_a.boxed_clone(),
5207                ix: 1,
5208                detail: 0,
5209                is_active: true,
5210            };
5211            pane.handle_tab_drop(&dragged_tab, 0, window, cx);
5212        });
5213
5214        // A should be before B and all are pinned
5215        assert_item_labels(&pane_a, ["A*!", "B!", "C!"], cx);
5216    }
5217
5218    #[gpui::test]
5219    async fn test_drag_first_tab_to_last_position(cx: &mut TestAppContext) {
5220        init_test(cx);
5221        let fs = FakeFs::new(cx.executor());
5222
5223        let project = Project::test(fs, None, cx).await;
5224        let (workspace, cx) =
5225            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
5226        let pane_a = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
5227
5228        // Add A, B, C
5229        let item_a = add_labeled_item(&pane_a, "A", false, cx);
5230        add_labeled_item(&pane_a, "B", false, cx);
5231        add_labeled_item(&pane_a, "C", false, cx);
5232        assert_item_labels(&pane_a, ["A", "B", "C*"], cx);
5233
5234        // Move A to the end
5235        pane_a.update_in(cx, |pane, window, cx| {
5236            let dragged_tab = DraggedTab {
5237                pane: pane_a.clone(),
5238                item: item_a.boxed_clone(),
5239                ix: 0,
5240                detail: 0,
5241                is_active: true,
5242            };
5243            pane.handle_tab_drop(&dragged_tab, 2, window, cx);
5244        });
5245
5246        // A should be at the end
5247        assert_item_labels(&pane_a, ["B", "C", "A*"], cx);
5248    }
5249
5250    #[gpui::test]
5251    async fn test_drag_last_tab_to_first_position(cx: &mut TestAppContext) {
5252        init_test(cx);
5253        let fs = FakeFs::new(cx.executor());
5254
5255        let project = Project::test(fs, None, cx).await;
5256        let (workspace, cx) =
5257            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
5258        let pane_a = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
5259
5260        // Add A, B, C
5261        add_labeled_item(&pane_a, "A", false, cx);
5262        add_labeled_item(&pane_a, "B", false, cx);
5263        let item_c = add_labeled_item(&pane_a, "C", false, cx);
5264        assert_item_labels(&pane_a, ["A", "B", "C*"], cx);
5265
5266        // Move C to the beginning
5267        pane_a.update_in(cx, |pane, window, cx| {
5268            let dragged_tab = DraggedTab {
5269                pane: pane_a.clone(),
5270                item: item_c.boxed_clone(),
5271                ix: 2,
5272                detail: 0,
5273                is_active: true,
5274            };
5275            pane.handle_tab_drop(&dragged_tab, 0, window, cx);
5276        });
5277
5278        // C should be at the beginning
5279        assert_item_labels(&pane_a, ["C*", "A", "B"], cx);
5280    }
5281
5282    #[gpui::test]
5283    async fn test_add_item_with_new_item(cx: &mut TestAppContext) {
5284        init_test(cx);
5285        let fs = FakeFs::new(cx.executor());
5286
5287        let project = Project::test(fs, None, cx).await;
5288        let (workspace, cx) =
5289            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
5290        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
5291
5292        // 1. Add with a destination index
5293        //   a. Add before the active item
5294        set_labeled_items(&pane, ["A", "B*", "C"], cx);
5295        pane.update_in(cx, |pane, window, cx| {
5296            pane.add_item(
5297                Box::new(cx.new(|cx| TestItem::new(cx).with_label("D"))),
5298                false,
5299                false,
5300                Some(0),
5301                window,
5302                cx,
5303            );
5304        });
5305        assert_item_labels(&pane, ["D*", "A", "B", "C"], cx);
5306
5307        //   b. Add after the active item
5308        set_labeled_items(&pane, ["A", "B*", "C"], cx);
5309        pane.update_in(cx, |pane, window, cx| {
5310            pane.add_item(
5311                Box::new(cx.new(|cx| TestItem::new(cx).with_label("D"))),
5312                false,
5313                false,
5314                Some(2),
5315                window,
5316                cx,
5317            );
5318        });
5319        assert_item_labels(&pane, ["A", "B", "D*", "C"], cx);
5320
5321        //   c. Add at the end of the item list (including off the length)
5322        set_labeled_items(&pane, ["A", "B*", "C"], cx);
5323        pane.update_in(cx, |pane, window, cx| {
5324            pane.add_item(
5325                Box::new(cx.new(|cx| TestItem::new(cx).with_label("D"))),
5326                false,
5327                false,
5328                Some(5),
5329                window,
5330                cx,
5331            );
5332        });
5333        assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
5334
5335        // 2. Add without a destination index
5336        //   a. Add with active item at the start of the item list
5337        set_labeled_items(&pane, ["A*", "B", "C"], cx);
5338        pane.update_in(cx, |pane, window, cx| {
5339            pane.add_item(
5340                Box::new(cx.new(|cx| TestItem::new(cx).with_label("D"))),
5341                false,
5342                false,
5343                None,
5344                window,
5345                cx,
5346            );
5347        });
5348        set_labeled_items(&pane, ["A", "D*", "B", "C"], cx);
5349
5350        //   b. Add with active item at the end of the item list
5351        set_labeled_items(&pane, ["A", "B", "C*"], cx);
5352        pane.update_in(cx, |pane, window, cx| {
5353            pane.add_item(
5354                Box::new(cx.new(|cx| TestItem::new(cx).with_label("D"))),
5355                false,
5356                false,
5357                None,
5358                window,
5359                cx,
5360            );
5361        });
5362        assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
5363    }
5364
5365    #[gpui::test]
5366    async fn test_add_item_with_existing_item(cx: &mut TestAppContext) {
5367        init_test(cx);
5368        let fs = FakeFs::new(cx.executor());
5369
5370        let project = Project::test(fs, None, cx).await;
5371        let (workspace, cx) =
5372            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
5373        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
5374
5375        // 1. Add with a destination index
5376        //   1a. Add before the active item
5377        let [_, _, _, d] = set_labeled_items(&pane, ["A", "B*", "C", "D"], cx);
5378        pane.update_in(cx, |pane, window, cx| {
5379            pane.add_item(d, false, false, Some(0), window, cx);
5380        });
5381        assert_item_labels(&pane, ["D*", "A", "B", "C"], cx);
5382
5383        //   1b. Add after the active item
5384        let [_, _, _, d] = set_labeled_items(&pane, ["A", "B*", "C", "D"], cx);
5385        pane.update_in(cx, |pane, window, cx| {
5386            pane.add_item(d, false, false, Some(2), window, cx);
5387        });
5388        assert_item_labels(&pane, ["A", "B", "D*", "C"], cx);
5389
5390        //   1c. Add at the end of the item list (including off the length)
5391        let [a, _, _, _] = set_labeled_items(&pane, ["A", "B*", "C", "D"], cx);
5392        pane.update_in(cx, |pane, window, cx| {
5393            pane.add_item(a, false, false, Some(5), window, cx);
5394        });
5395        assert_item_labels(&pane, ["B", "C", "D", "A*"], cx);
5396
5397        //   1d. Add same item to active index
5398        let [_, b, _] = set_labeled_items(&pane, ["A", "B*", "C"], cx);
5399        pane.update_in(cx, |pane, window, cx| {
5400            pane.add_item(b, false, false, Some(1), window, cx);
5401        });
5402        assert_item_labels(&pane, ["A", "B*", "C"], cx);
5403
5404        //   1e. Add item to index after same item in last position
5405        let [_, _, c] = set_labeled_items(&pane, ["A", "B*", "C"], cx);
5406        pane.update_in(cx, |pane, window, cx| {
5407            pane.add_item(c, false, false, Some(2), window, cx);
5408        });
5409        assert_item_labels(&pane, ["A", "B", "C*"], cx);
5410
5411        // 2. Add without a destination index
5412        //   2a. Add with active item at the start of the item list
5413        let [_, _, _, d] = set_labeled_items(&pane, ["A*", "B", "C", "D"], cx);
5414        pane.update_in(cx, |pane, window, cx| {
5415            pane.add_item(d, false, false, None, window, cx);
5416        });
5417        assert_item_labels(&pane, ["A", "D*", "B", "C"], cx);
5418
5419        //   2b. Add with active item at the end of the item list
5420        let [a, _, _, _] = set_labeled_items(&pane, ["A", "B", "C", "D*"], cx);
5421        pane.update_in(cx, |pane, window, cx| {
5422            pane.add_item(a, false, false, None, window, cx);
5423        });
5424        assert_item_labels(&pane, ["B", "C", "D", "A*"], cx);
5425
5426        //   2c. Add active item to active item at end of list
5427        let [_, _, c] = set_labeled_items(&pane, ["A", "B", "C*"], cx);
5428        pane.update_in(cx, |pane, window, cx| {
5429            pane.add_item(c, false, false, None, window, cx);
5430        });
5431        assert_item_labels(&pane, ["A", "B", "C*"], cx);
5432
5433        //   2d. Add active item to active item at start of list
5434        let [a, _, _] = set_labeled_items(&pane, ["A*", "B", "C"], cx);
5435        pane.update_in(cx, |pane, window, cx| {
5436            pane.add_item(a, false, false, None, window, cx);
5437        });
5438        assert_item_labels(&pane, ["A*", "B", "C"], cx);
5439    }
5440
5441    #[gpui::test]
5442    async fn test_add_item_with_same_project_entries(cx: &mut TestAppContext) {
5443        init_test(cx);
5444        let fs = FakeFs::new(cx.executor());
5445
5446        let project = Project::test(fs, None, cx).await;
5447        let (workspace, cx) =
5448            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
5449        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
5450
5451        // singleton view
5452        pane.update_in(cx, |pane, window, cx| {
5453            pane.add_item(
5454                Box::new(cx.new(|cx| {
5455                    TestItem::new(cx)
5456                        .with_singleton(true)
5457                        .with_label("buffer 1")
5458                        .with_project_items(&[TestProjectItem::new(1, "one.txt", cx)])
5459                })),
5460                false,
5461                false,
5462                None,
5463                window,
5464                cx,
5465            );
5466        });
5467        assert_item_labels(&pane, ["buffer 1*"], cx);
5468
5469        // new singleton view with the same project entry
5470        pane.update_in(cx, |pane, window, cx| {
5471            pane.add_item(
5472                Box::new(cx.new(|cx| {
5473                    TestItem::new(cx)
5474                        .with_singleton(true)
5475                        .with_label("buffer 1")
5476                        .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
5477                })),
5478                false,
5479                false,
5480                None,
5481                window,
5482                cx,
5483            );
5484        });
5485        assert_item_labels(&pane, ["buffer 1*"], cx);
5486
5487        // new singleton view with different project entry
5488        pane.update_in(cx, |pane, window, cx| {
5489            pane.add_item(
5490                Box::new(cx.new(|cx| {
5491                    TestItem::new(cx)
5492                        .with_singleton(true)
5493                        .with_label("buffer 2")
5494                        .with_project_items(&[TestProjectItem::new(2, "2.txt", cx)])
5495                })),
5496                false,
5497                false,
5498                None,
5499                window,
5500                cx,
5501            );
5502        });
5503        assert_item_labels(&pane, ["buffer 1", "buffer 2*"], cx);
5504
5505        // new multibuffer view with the same project entry
5506        pane.update_in(cx, |pane, window, cx| {
5507            pane.add_item(
5508                Box::new(cx.new(|cx| {
5509                    TestItem::new(cx)
5510                        .with_singleton(false)
5511                        .with_label("multibuffer 1")
5512                        .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
5513                })),
5514                false,
5515                false,
5516                None,
5517                window,
5518                cx,
5519            );
5520        });
5521        assert_item_labels(&pane, ["buffer 1", "buffer 2", "multibuffer 1*"], cx);
5522
5523        // another multibuffer view with the same project entry
5524        pane.update_in(cx, |pane, window, cx| {
5525            pane.add_item(
5526                Box::new(cx.new(|cx| {
5527                    TestItem::new(cx)
5528                        .with_singleton(false)
5529                        .with_label("multibuffer 1b")
5530                        .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
5531                })),
5532                false,
5533                false,
5534                None,
5535                window,
5536                cx,
5537            );
5538        });
5539        assert_item_labels(
5540            &pane,
5541            ["buffer 1", "buffer 2", "multibuffer 1", "multibuffer 1b*"],
5542            cx,
5543        );
5544    }
5545
5546    #[gpui::test]
5547    async fn test_remove_item_ordering_history(cx: &mut TestAppContext) {
5548        init_test(cx);
5549        let fs = FakeFs::new(cx.executor());
5550
5551        let project = Project::test(fs, None, cx).await;
5552        let (workspace, cx) =
5553            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
5554        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
5555
5556        add_labeled_item(&pane, "A", false, cx);
5557        add_labeled_item(&pane, "B", false, cx);
5558        add_labeled_item(&pane, "C", false, cx);
5559        add_labeled_item(&pane, "D", false, cx);
5560        assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
5561
5562        pane.update_in(cx, |pane, window, cx| {
5563            pane.activate_item(1, false, false, window, cx)
5564        });
5565        add_labeled_item(&pane, "1", false, cx);
5566        assert_item_labels(&pane, ["A", "B", "1*", "C", "D"], cx);
5567
5568        pane.update_in(cx, |pane, window, cx| {
5569            pane.close_active_item(
5570                &CloseActiveItem {
5571                    save_intent: None,
5572                    close_pinned: false,
5573                },
5574                window,
5575                cx,
5576            )
5577        })
5578        .await
5579        .unwrap();
5580        assert_item_labels(&pane, ["A", "B*", "C", "D"], cx);
5581
5582        pane.update_in(cx, |pane, window, cx| {
5583            pane.activate_item(3, false, false, window, cx)
5584        });
5585        assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
5586
5587        pane.update_in(cx, |pane, window, cx| {
5588            pane.close_active_item(
5589                &CloseActiveItem {
5590                    save_intent: None,
5591                    close_pinned: false,
5592                },
5593                window,
5594                cx,
5595            )
5596        })
5597        .await
5598        .unwrap();
5599        assert_item_labels(&pane, ["A", "B*", "C"], cx);
5600
5601        pane.update_in(cx, |pane, window, cx| {
5602            pane.close_active_item(
5603                &CloseActiveItem {
5604                    save_intent: None,
5605                    close_pinned: false,
5606                },
5607                window,
5608                cx,
5609            )
5610        })
5611        .await
5612        .unwrap();
5613        assert_item_labels(&pane, ["A", "C*"], cx);
5614
5615        pane.update_in(cx, |pane, window, cx| {
5616            pane.close_active_item(
5617                &CloseActiveItem {
5618                    save_intent: None,
5619                    close_pinned: false,
5620                },
5621                window,
5622                cx,
5623            )
5624        })
5625        .await
5626        .unwrap();
5627        assert_item_labels(&pane, ["A*"], cx);
5628    }
5629
5630    #[gpui::test]
5631    async fn test_remove_item_ordering_neighbour(cx: &mut TestAppContext) {
5632        init_test(cx);
5633        cx.update_global::<SettingsStore, ()>(|s, cx| {
5634            s.update_user_settings::<ItemSettings>(cx, |s| {
5635                s.activate_on_close = Some(ActivateOnClose::Neighbour);
5636            });
5637        });
5638        let fs = FakeFs::new(cx.executor());
5639
5640        let project = Project::test(fs, None, cx).await;
5641        let (workspace, cx) =
5642            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
5643        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
5644
5645        add_labeled_item(&pane, "A", false, cx);
5646        add_labeled_item(&pane, "B", false, cx);
5647        add_labeled_item(&pane, "C", false, cx);
5648        add_labeled_item(&pane, "D", false, cx);
5649        assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
5650
5651        pane.update_in(cx, |pane, window, cx| {
5652            pane.activate_item(1, false, false, window, cx)
5653        });
5654        add_labeled_item(&pane, "1", false, cx);
5655        assert_item_labels(&pane, ["A", "B", "1*", "C", "D"], cx);
5656
5657        pane.update_in(cx, |pane, window, cx| {
5658            pane.close_active_item(
5659                &CloseActiveItem {
5660                    save_intent: None,
5661                    close_pinned: false,
5662                },
5663                window,
5664                cx,
5665            )
5666        })
5667        .await
5668        .unwrap();
5669        assert_item_labels(&pane, ["A", "B", "C*", "D"], cx);
5670
5671        pane.update_in(cx, |pane, window, cx| {
5672            pane.activate_item(3, false, false, window, cx)
5673        });
5674        assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
5675
5676        pane.update_in(cx, |pane, window, cx| {
5677            pane.close_active_item(
5678                &CloseActiveItem {
5679                    save_intent: None,
5680                    close_pinned: false,
5681                },
5682                window,
5683                cx,
5684            )
5685        })
5686        .await
5687        .unwrap();
5688        assert_item_labels(&pane, ["A", "B", "C*"], cx);
5689
5690        pane.update_in(cx, |pane, window, cx| {
5691            pane.close_active_item(
5692                &CloseActiveItem {
5693                    save_intent: None,
5694                    close_pinned: false,
5695                },
5696                window,
5697                cx,
5698            )
5699        })
5700        .await
5701        .unwrap();
5702        assert_item_labels(&pane, ["A", "B*"], cx);
5703
5704        pane.update_in(cx, |pane, window, cx| {
5705            pane.close_active_item(
5706                &CloseActiveItem {
5707                    save_intent: None,
5708                    close_pinned: false,
5709                },
5710                window,
5711                cx,
5712            )
5713        })
5714        .await
5715        .unwrap();
5716        assert_item_labels(&pane, ["A*"], cx);
5717    }
5718
5719    #[gpui::test]
5720    async fn test_remove_item_ordering_left_neighbour(cx: &mut TestAppContext) {
5721        init_test(cx);
5722        cx.update_global::<SettingsStore, ()>(|s, cx| {
5723            s.update_user_settings::<ItemSettings>(cx, |s| {
5724                s.activate_on_close = Some(ActivateOnClose::LeftNeighbour);
5725            });
5726        });
5727        let fs = FakeFs::new(cx.executor());
5728
5729        let project = Project::test(fs, None, cx).await;
5730        let (workspace, cx) =
5731            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
5732        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
5733
5734        add_labeled_item(&pane, "A", false, cx);
5735        add_labeled_item(&pane, "B", false, cx);
5736        add_labeled_item(&pane, "C", false, cx);
5737        add_labeled_item(&pane, "D", false, cx);
5738        assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
5739
5740        pane.update_in(cx, |pane, window, cx| {
5741            pane.activate_item(1, false, false, window, cx)
5742        });
5743        add_labeled_item(&pane, "1", false, cx);
5744        assert_item_labels(&pane, ["A", "B", "1*", "C", "D"], cx);
5745
5746        pane.update_in(cx, |pane, window, cx| {
5747            pane.close_active_item(
5748                &CloseActiveItem {
5749                    save_intent: None,
5750                    close_pinned: false,
5751                },
5752                window,
5753                cx,
5754            )
5755        })
5756        .await
5757        .unwrap();
5758        assert_item_labels(&pane, ["A", "B*", "C", "D"], cx);
5759
5760        pane.update_in(cx, |pane, window, cx| {
5761            pane.activate_item(3, false, false, window, cx)
5762        });
5763        assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
5764
5765        pane.update_in(cx, |pane, window, cx| {
5766            pane.close_active_item(
5767                &CloseActiveItem {
5768                    save_intent: None,
5769                    close_pinned: false,
5770                },
5771                window,
5772                cx,
5773            )
5774        })
5775        .await
5776        .unwrap();
5777        assert_item_labels(&pane, ["A", "B", "C*"], cx);
5778
5779        pane.update_in(cx, |pane, window, cx| {
5780            pane.activate_item(0, false, false, window, cx)
5781        });
5782        assert_item_labels(&pane, ["A*", "B", "C"], cx);
5783
5784        pane.update_in(cx, |pane, window, cx| {
5785            pane.close_active_item(
5786                &CloseActiveItem {
5787                    save_intent: None,
5788                    close_pinned: false,
5789                },
5790                window,
5791                cx,
5792            )
5793        })
5794        .await
5795        .unwrap();
5796        assert_item_labels(&pane, ["B*", "C"], cx);
5797
5798        pane.update_in(cx, |pane, window, cx| {
5799            pane.close_active_item(
5800                &CloseActiveItem {
5801                    save_intent: None,
5802                    close_pinned: false,
5803                },
5804                window,
5805                cx,
5806            )
5807        })
5808        .await
5809        .unwrap();
5810        assert_item_labels(&pane, ["C*"], cx);
5811    }
5812
5813    #[gpui::test]
5814    async fn test_close_inactive_items(cx: &mut TestAppContext) {
5815        init_test(cx);
5816        let fs = FakeFs::new(cx.executor());
5817
5818        let project = Project::test(fs, None, cx).await;
5819        let (workspace, cx) =
5820            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
5821        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
5822
5823        let item_a = add_labeled_item(&pane, "A", false, cx);
5824        pane.update_in(cx, |pane, window, cx| {
5825            let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
5826            pane.pin_tab_at(ix, window, cx);
5827        });
5828        assert_item_labels(&pane, ["A*!"], cx);
5829
5830        let item_b = add_labeled_item(&pane, "B", false, cx);
5831        pane.update_in(cx, |pane, window, cx| {
5832            let ix = pane.index_for_item_id(item_b.item_id()).unwrap();
5833            pane.pin_tab_at(ix, window, cx);
5834        });
5835        assert_item_labels(&pane, ["A!", "B*!"], cx);
5836
5837        add_labeled_item(&pane, "C", false, cx);
5838        assert_item_labels(&pane, ["A!", "B!", "C*"], cx);
5839
5840        add_labeled_item(&pane, "D", false, cx);
5841        add_labeled_item(&pane, "E", false, cx);
5842        assert_item_labels(&pane, ["A!", "B!", "C", "D", "E*"], cx);
5843
5844        pane.update_in(cx, |pane, window, cx| {
5845            pane.close_inactive_items(
5846                &CloseInactiveItems {
5847                    save_intent: None,
5848                    close_pinned: false,
5849                },
5850                None,
5851                window,
5852                cx,
5853            )
5854        })
5855        .await
5856        .unwrap();
5857        assert_item_labels(&pane, ["A!", "B!", "E*"], cx);
5858    }
5859
5860    #[gpui::test]
5861    async fn test_close_clean_items(cx: &mut TestAppContext) {
5862        init_test(cx);
5863        let fs = FakeFs::new(cx.executor());
5864
5865        let project = Project::test(fs, None, cx).await;
5866        let (workspace, cx) =
5867            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
5868        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
5869
5870        add_labeled_item(&pane, "A", true, cx);
5871        add_labeled_item(&pane, "B", false, cx);
5872        add_labeled_item(&pane, "C", true, cx);
5873        add_labeled_item(&pane, "D", false, cx);
5874        add_labeled_item(&pane, "E", false, cx);
5875        assert_item_labels(&pane, ["A^", "B", "C^", "D", "E*"], cx);
5876
5877        pane.update_in(cx, |pane, window, cx| {
5878            pane.close_clean_items(
5879                &CloseCleanItems {
5880                    close_pinned: false,
5881                },
5882                window,
5883                cx,
5884            )
5885        })
5886        .await
5887        .unwrap();
5888        assert_item_labels(&pane, ["A^", "C*^"], cx);
5889    }
5890
5891    #[gpui::test]
5892    async fn test_close_items_to_the_left(cx: &mut TestAppContext) {
5893        init_test(cx);
5894        let fs = FakeFs::new(cx.executor());
5895
5896        let project = Project::test(fs, None, cx).await;
5897        let (workspace, cx) =
5898            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
5899        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
5900
5901        set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx);
5902
5903        pane.update_in(cx, |pane, window, cx| {
5904            pane.close_items_to_the_left_by_id(
5905                None,
5906                &CloseItemsToTheLeft {
5907                    close_pinned: false,
5908                },
5909                window,
5910                cx,
5911            )
5912        })
5913        .await
5914        .unwrap();
5915        assert_item_labels(&pane, ["C*", "D", "E"], cx);
5916    }
5917
5918    #[gpui::test]
5919    async fn test_close_items_to_the_right(cx: &mut TestAppContext) {
5920        init_test(cx);
5921        let fs = FakeFs::new(cx.executor());
5922
5923        let project = Project::test(fs, None, cx).await;
5924        let (workspace, cx) =
5925            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
5926        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
5927
5928        set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx);
5929
5930        pane.update_in(cx, |pane, window, cx| {
5931            pane.close_items_to_the_right_by_id(
5932                None,
5933                &CloseItemsToTheRight {
5934                    close_pinned: false,
5935                },
5936                window,
5937                cx,
5938            )
5939        })
5940        .await
5941        .unwrap();
5942        assert_item_labels(&pane, ["A", "B", "C*"], cx);
5943    }
5944
5945    #[gpui::test]
5946    async fn test_close_all_items(cx: &mut TestAppContext) {
5947        init_test(cx);
5948        let fs = FakeFs::new(cx.executor());
5949
5950        let project = Project::test(fs, None, cx).await;
5951        let (workspace, cx) =
5952            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
5953        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
5954
5955        let item_a = add_labeled_item(&pane, "A", false, cx);
5956        add_labeled_item(&pane, "B", false, cx);
5957        add_labeled_item(&pane, "C", false, cx);
5958        assert_item_labels(&pane, ["A", "B", "C*"], cx);
5959
5960        pane.update_in(cx, |pane, window, cx| {
5961            let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
5962            pane.pin_tab_at(ix, window, cx);
5963            pane.close_all_items(
5964                &CloseAllItems {
5965                    save_intent: None,
5966                    close_pinned: false,
5967                },
5968                window,
5969                cx,
5970            )
5971        })
5972        .await
5973        .unwrap();
5974        assert_item_labels(&pane, ["A*!"], cx);
5975
5976        pane.update_in(cx, |pane, window, cx| {
5977            let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
5978            pane.unpin_tab_at(ix, window, cx);
5979            pane.close_all_items(
5980                &CloseAllItems {
5981                    save_intent: None,
5982                    close_pinned: false,
5983                },
5984                window,
5985                cx,
5986            )
5987        })
5988        .await
5989        .unwrap();
5990
5991        assert_item_labels(&pane, [], cx);
5992
5993        add_labeled_item(&pane, "A", true, cx).update(cx, |item, cx| {
5994            item.project_items
5995                .push(TestProjectItem::new_dirty(1, "A.txt", cx))
5996        });
5997        add_labeled_item(&pane, "B", true, cx).update(cx, |item, cx| {
5998            item.project_items
5999                .push(TestProjectItem::new_dirty(2, "B.txt", cx))
6000        });
6001        add_labeled_item(&pane, "C", true, cx).update(cx, |item, cx| {
6002            item.project_items
6003                .push(TestProjectItem::new_dirty(3, "C.txt", cx))
6004        });
6005        assert_item_labels(&pane, ["A^", "B^", "C*^"], cx);
6006
6007        let save = pane.update_in(cx, |pane, window, cx| {
6008            pane.close_all_items(
6009                &CloseAllItems {
6010                    save_intent: None,
6011                    close_pinned: false,
6012                },
6013                window,
6014                cx,
6015            )
6016        });
6017
6018        cx.executor().run_until_parked();
6019        cx.simulate_prompt_answer("Save all");
6020        save.await.unwrap();
6021        assert_item_labels(&pane, [], cx);
6022
6023        add_labeled_item(&pane, "A", true, cx);
6024        add_labeled_item(&pane, "B", true, cx);
6025        add_labeled_item(&pane, "C", true, cx);
6026        assert_item_labels(&pane, ["A^", "B^", "C*^"], cx);
6027        let save = pane.update_in(cx, |pane, window, cx| {
6028            pane.close_all_items(
6029                &CloseAllItems {
6030                    save_intent: None,
6031                    close_pinned: false,
6032                },
6033                window,
6034                cx,
6035            )
6036        });
6037
6038        cx.executor().run_until_parked();
6039        cx.simulate_prompt_answer("Discard all");
6040        save.await.unwrap();
6041        assert_item_labels(&pane, [], cx);
6042    }
6043
6044    #[gpui::test]
6045    async fn test_close_with_save_intent(cx: &mut TestAppContext) {
6046        init_test(cx);
6047        let fs = FakeFs::new(cx.executor());
6048
6049        let project = Project::test(fs, None, cx).await;
6050        let (workspace, cx) =
6051            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
6052        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
6053
6054        let a = cx.update(|_, cx| TestProjectItem::new_dirty(1, "A.txt", cx));
6055        let b = cx.update(|_, cx| TestProjectItem::new_dirty(1, "B.txt", cx));
6056        let c = cx.update(|_, cx| TestProjectItem::new_dirty(1, "C.txt", cx));
6057
6058        add_labeled_item(&pane, "AB", true, cx).update(cx, |item, _| {
6059            item.project_items.push(a.clone());
6060            item.project_items.push(b.clone());
6061        });
6062        add_labeled_item(&pane, "C", true, cx)
6063            .update(cx, |item, _| item.project_items.push(c.clone()));
6064        assert_item_labels(&pane, ["AB^", "C*^"], cx);
6065
6066        pane.update_in(cx, |pane, window, cx| {
6067            pane.close_all_items(
6068                &CloseAllItems {
6069                    save_intent: Some(SaveIntent::Save),
6070                    close_pinned: false,
6071                },
6072                window,
6073                cx,
6074            )
6075        })
6076        .await
6077        .unwrap();
6078
6079        assert_item_labels(&pane, [], cx);
6080        cx.update(|_, cx| {
6081            assert!(!a.read(cx).is_dirty);
6082            assert!(!b.read(cx).is_dirty);
6083            assert!(!c.read(cx).is_dirty);
6084        });
6085    }
6086
6087    #[gpui::test]
6088    async fn test_close_all_items_including_pinned(cx: &mut TestAppContext) {
6089        init_test(cx);
6090        let fs = FakeFs::new(cx.executor());
6091
6092        let project = Project::test(fs, None, cx).await;
6093        let (workspace, cx) =
6094            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
6095        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
6096
6097        let item_a = add_labeled_item(&pane, "A", false, cx);
6098        add_labeled_item(&pane, "B", false, cx);
6099        add_labeled_item(&pane, "C", false, cx);
6100        assert_item_labels(&pane, ["A", "B", "C*"], cx);
6101
6102        pane.update_in(cx, |pane, window, cx| {
6103            let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
6104            pane.pin_tab_at(ix, window, cx);
6105            pane.close_all_items(
6106                &CloseAllItems {
6107                    save_intent: None,
6108                    close_pinned: true,
6109                },
6110                window,
6111                cx,
6112            )
6113        })
6114        .await
6115        .unwrap();
6116        assert_item_labels(&pane, [], cx);
6117    }
6118
6119    #[gpui::test]
6120    async fn test_close_pinned_tab_with_non_pinned_in_same_pane(cx: &mut TestAppContext) {
6121        init_test(cx);
6122        let fs = FakeFs::new(cx.executor());
6123        let project = Project::test(fs, None, cx).await;
6124        let (workspace, cx) =
6125            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
6126
6127        // Non-pinned tabs in same pane
6128        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
6129        add_labeled_item(&pane, "A", false, cx);
6130        add_labeled_item(&pane, "B", false, cx);
6131        add_labeled_item(&pane, "C", false, cx);
6132        pane.update_in(cx, |pane, window, cx| {
6133            pane.pin_tab_at(0, window, cx);
6134        });
6135        set_labeled_items(&pane, ["A*", "B", "C"], cx);
6136        pane.update_in(cx, |pane, window, cx| {
6137            pane.close_active_item(
6138                &CloseActiveItem {
6139                    save_intent: None,
6140                    close_pinned: false,
6141                },
6142                window,
6143                cx,
6144            )
6145            .unwrap();
6146        });
6147        // Non-pinned tab should be active
6148        assert_item_labels(&pane, ["A!", "B*", "C"], cx);
6149    }
6150
6151    #[gpui::test]
6152    async fn test_close_pinned_tab_with_non_pinned_in_different_pane(cx: &mut TestAppContext) {
6153        init_test(cx);
6154        let fs = FakeFs::new(cx.executor());
6155        let project = Project::test(fs, None, cx).await;
6156        let (workspace, cx) =
6157            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
6158
6159        // No non-pinned tabs in same pane, non-pinned tabs in another pane
6160        let pane1 = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
6161        let pane2 = workspace.update_in(cx, |workspace, window, cx| {
6162            workspace.split_pane(pane1.clone(), SplitDirection::Right, window, cx)
6163        });
6164        add_labeled_item(&pane1, "A", false, cx);
6165        pane1.update_in(cx, |pane, window, cx| {
6166            pane.pin_tab_at(0, window, cx);
6167        });
6168        set_labeled_items(&pane1, ["A*"], cx);
6169        add_labeled_item(&pane2, "B", false, cx);
6170        set_labeled_items(&pane2, ["B"], cx);
6171        pane1.update_in(cx, |pane, window, cx| {
6172            pane.close_active_item(
6173                &CloseActiveItem {
6174                    save_intent: None,
6175                    close_pinned: false,
6176                },
6177                window,
6178                cx,
6179            )
6180            .unwrap();
6181        });
6182        //  Non-pinned tab of other pane should be active
6183        assert_item_labels(&pane2, ["B*"], cx);
6184    }
6185
6186    #[gpui::test]
6187    async fn ensure_item_closing_actions_do_not_panic_when_no_items_exist(cx: &mut TestAppContext) {
6188        init_test(cx);
6189        let fs = FakeFs::new(cx.executor());
6190        let project = Project::test(fs, None, cx).await;
6191        let (workspace, cx) =
6192            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
6193
6194        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
6195        assert_item_labels(&pane, [], cx);
6196
6197        pane.update_in(cx, |pane, window, cx| {
6198            pane.close_active_item(
6199                &CloseActiveItem {
6200                    save_intent: None,
6201                    close_pinned: false,
6202                },
6203                window,
6204                cx,
6205            )
6206        })
6207        .await
6208        .unwrap();
6209
6210        pane.update_in(cx, |pane, window, cx| {
6211            pane.close_inactive_items(
6212                &CloseInactiveItems {
6213                    save_intent: None,
6214                    close_pinned: false,
6215                },
6216                None,
6217                window,
6218                cx,
6219            )
6220        })
6221        .await
6222        .unwrap();
6223
6224        pane.update_in(cx, |pane, window, cx| {
6225            pane.close_all_items(
6226                &CloseAllItems {
6227                    save_intent: None,
6228                    close_pinned: false,
6229                },
6230                window,
6231                cx,
6232            )
6233        })
6234        .await
6235        .unwrap();
6236
6237        pane.update_in(cx, |pane, window, cx| {
6238            pane.close_clean_items(
6239                &CloseCleanItems {
6240                    close_pinned: false,
6241                },
6242                window,
6243                cx,
6244            )
6245        })
6246        .await
6247        .unwrap();
6248
6249        pane.update_in(cx, |pane, window, cx| {
6250            pane.close_items_to_the_right_by_id(
6251                None,
6252                &CloseItemsToTheRight {
6253                    close_pinned: false,
6254                },
6255                window,
6256                cx,
6257            )
6258        })
6259        .await
6260        .unwrap();
6261
6262        pane.update_in(cx, |pane, window, cx| {
6263            pane.close_items_to_the_left_by_id(
6264                None,
6265                &CloseItemsToTheLeft {
6266                    close_pinned: false,
6267                },
6268                window,
6269                cx,
6270            )
6271        })
6272        .await
6273        .unwrap();
6274    }
6275
6276    fn init_test(cx: &mut TestAppContext) {
6277        cx.update(|cx| {
6278            let settings_store = SettingsStore::test(cx);
6279            cx.set_global(settings_store);
6280            theme::init(LoadThemes::JustBase, cx);
6281            crate::init_settings(cx);
6282            Project::init_settings(cx);
6283        });
6284    }
6285
6286    fn set_max_tabs(cx: &mut TestAppContext, value: Option<usize>) {
6287        cx.update_global(|store: &mut SettingsStore, cx| {
6288            store.update_user_settings::<WorkspaceSettings>(cx, |settings| {
6289                settings.max_tabs = value.map(|v| NonZero::new(v).unwrap())
6290            });
6291        });
6292    }
6293
6294    fn add_labeled_item(
6295        pane: &Entity<Pane>,
6296        label: &str,
6297        is_dirty: bool,
6298        cx: &mut VisualTestContext,
6299    ) -> Box<Entity<TestItem>> {
6300        pane.update_in(cx, |pane, window, cx| {
6301            let labeled_item =
6302                Box::new(cx.new(|cx| TestItem::new(cx).with_label(label).with_dirty(is_dirty)));
6303            pane.add_item(labeled_item.clone(), false, false, None, window, cx);
6304            labeled_item
6305        })
6306    }
6307
6308    fn set_labeled_items<const COUNT: usize>(
6309        pane: &Entity<Pane>,
6310        labels: [&str; COUNT],
6311        cx: &mut VisualTestContext,
6312    ) -> [Box<Entity<TestItem>>; COUNT] {
6313        pane.update_in(cx, |pane, window, cx| {
6314            pane.items.clear();
6315            let mut active_item_index = 0;
6316
6317            let mut index = 0;
6318            let items = labels.map(|mut label| {
6319                if label.ends_with('*') {
6320                    label = label.trim_end_matches('*');
6321                    active_item_index = index;
6322                }
6323
6324                let labeled_item = Box::new(cx.new(|cx| TestItem::new(cx).with_label(label)));
6325                pane.add_item(labeled_item.clone(), false, false, None, window, cx);
6326                index += 1;
6327                labeled_item
6328            });
6329
6330            pane.activate_item(active_item_index, false, false, window, cx);
6331
6332            items
6333        })
6334    }
6335
6336    // Assert the item label, with the active item label suffixed with a '*'
6337    #[track_caller]
6338    fn assert_item_labels<const COUNT: usize>(
6339        pane: &Entity<Pane>,
6340        expected_states: [&str; COUNT],
6341        cx: &mut VisualTestContext,
6342    ) {
6343        let actual_states = pane.update(cx, |pane, cx| {
6344            pane.items
6345                .iter()
6346                .enumerate()
6347                .map(|(ix, item)| {
6348                    let mut state = item
6349                        .to_any()
6350                        .downcast::<TestItem>()
6351                        .unwrap()
6352                        .read(cx)
6353                        .label
6354                        .clone();
6355                    if ix == pane.active_item_index {
6356                        state.push('*');
6357                    }
6358                    if item.is_dirty(cx) {
6359                        state.push('^');
6360                    }
6361                    if pane.is_tab_pinned(ix) {
6362                        state.push('!');
6363                    }
6364                    state
6365                })
6366                .collect::<Vec<_>>()
6367        });
6368        assert_eq!(
6369            actual_states, expected_states,
6370            "pane items do not match expectation"
6371        );
6372    }
6373}