pane.rs

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