pane.rs

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