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