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::{DirectoryLister, Project, ProjectEntryId, ProjectPath, WorktreeId};
  29use schemars::JsonSchema;
  30use serde::Deserialize;
  31use settings::{Settings, SettingsStore};
  32use std::{
  33    any::Any,
  34    cmp, fmt, mem,
  35    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 new_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                        let lister = if workspace.project().read(cx).is_local() {
1928                            DirectoryLister::Local(
1929                                workspace.project().clone(),
1930                                workspace.app_state().fs.clone(),
1931                            )
1932                        } else {
1933                            DirectoryLister::Project(workspace.project().clone())
1934                        };
1935                        workspace.prompt_for_new_path(lister, window, cx)
1936                    })
1937                })??;
1938                let Some(new_path) = new_path.await.ok().flatten().into_iter().flatten().next()
1939                else {
1940                    return Ok(false);
1941                };
1942
1943                let project_path = pane
1944                    .update(cx, |pane, cx| {
1945                        pane.project
1946                            .update(cx, |project, cx| {
1947                                project.find_or_create_worktree(new_path, true, cx)
1948                            })
1949                            .ok()
1950                    })
1951                    .ok()
1952                    .flatten();
1953                let save_task = if let Some(project_path) = project_path {
1954                    let (worktree, path) = project_path.await?;
1955                    let worktree_id = worktree.read_with(cx, |worktree, _| worktree.id())?;
1956                    let new_path = ProjectPath {
1957                        worktree_id,
1958                        path: path.into(),
1959                    };
1960
1961                    pane.update_in(cx, |pane, window, cx| {
1962                        if let Some(item) = pane.item_for_path(new_path.clone(), cx) {
1963                            pane.remove_item(item.item_id(), false, false, window, cx);
1964                        }
1965
1966                        item.save_as(project, new_path, window, cx)
1967                    })?
1968                } else {
1969                    return Ok(false);
1970                };
1971
1972                save_task.await?;
1973                return Ok(true);
1974            }
1975        }
1976
1977        pane.update(cx, |_, cx| {
1978            cx.emit(Event::UserSavedItem {
1979                item: item.downgrade_item(),
1980                save_intent,
1981            });
1982            true
1983        })
1984    }
1985
1986    pub fn autosave_item(
1987        item: &dyn ItemHandle,
1988        project: Entity<Project>,
1989        window: &mut Window,
1990        cx: &mut App,
1991    ) -> Task<Result<()>> {
1992        let format = !matches!(
1993            item.workspace_settings(cx).autosave,
1994            AutosaveSetting::AfterDelay { .. }
1995        );
1996        if item.can_autosave(cx) {
1997            item.save(format, project, window, cx)
1998        } else {
1999            Task::ready(Ok(()))
2000        }
2001    }
2002
2003    pub fn focus_active_item(&mut self, window: &mut Window, cx: &mut Context<Self>) {
2004        if let Some(active_item) = self.active_item() {
2005            let focus_handle = active_item.item_focus_handle(cx);
2006            window.focus(&focus_handle);
2007        }
2008    }
2009
2010    pub fn split(&mut self, direction: SplitDirection, cx: &mut Context<Self>) {
2011        cx.emit(Event::Split(direction));
2012    }
2013
2014    pub fn toolbar(&self) -> &Entity<Toolbar> {
2015        &self.toolbar
2016    }
2017
2018    pub fn handle_deleted_project_item(
2019        &mut self,
2020        entry_id: ProjectEntryId,
2021        window: &mut Window,
2022        cx: &mut Context<Pane>,
2023    ) -> Option<()> {
2024        let item_id = self.items().find_map(|item| {
2025            if item.is_singleton(cx) && item.project_entry_ids(cx).as_slice() == [entry_id] {
2026                Some(item.item_id())
2027            } else {
2028                None
2029            }
2030        })?;
2031
2032        self.remove_item(item_id, false, true, window, cx);
2033        self.nav_history.remove_item(item_id);
2034
2035        Some(())
2036    }
2037
2038    fn update_toolbar(&mut self, window: &mut Window, cx: &mut Context<Self>) {
2039        let active_item = self
2040            .items
2041            .get(self.active_item_index)
2042            .map(|item| item.as_ref());
2043        self.toolbar.update(cx, |toolbar, cx| {
2044            toolbar.set_active_item(active_item, window, cx);
2045        });
2046    }
2047
2048    fn update_status_bar(&mut self, window: &mut Window, cx: &mut Context<Self>) {
2049        let workspace = self.workspace.clone();
2050        let pane = cx.entity().clone();
2051
2052        window.defer(cx, move |window, cx| {
2053            let Ok(status_bar) =
2054                workspace.read_with(cx, |workspace, _| workspace.status_bar.clone())
2055            else {
2056                return;
2057            };
2058
2059            status_bar.update(cx, move |status_bar, cx| {
2060                status_bar.set_active_pane(&pane, window, cx);
2061            });
2062        });
2063    }
2064
2065    fn entry_abs_path(&self, entry: ProjectEntryId, cx: &App) -> Option<PathBuf> {
2066        let worktree = self
2067            .workspace
2068            .upgrade()?
2069            .read(cx)
2070            .project()
2071            .read(cx)
2072            .worktree_for_entry(entry, cx)?
2073            .read(cx);
2074        let entry = worktree.entry_for_id(entry)?;
2075        match &entry.canonical_path {
2076            Some(canonical_path) => Some(canonical_path.to_path_buf()),
2077            None => worktree.absolutize(&entry.path).ok(),
2078        }
2079    }
2080
2081    pub fn icon_color(selected: bool) -> Color {
2082        if selected {
2083            Color::Default
2084        } else {
2085            Color::Muted
2086        }
2087    }
2088
2089    fn toggle_pin_tab(&mut self, _: &TogglePinTab, window: &mut Window, cx: &mut Context<Self>) {
2090        if self.items.is_empty() {
2091            return;
2092        }
2093        let active_tab_ix = self.active_item_index();
2094        if self.is_tab_pinned(active_tab_ix) {
2095            self.unpin_tab_at(active_tab_ix, window, cx);
2096        } else {
2097            self.pin_tab_at(active_tab_ix, window, cx);
2098        }
2099    }
2100
2101    fn pin_tab_at(&mut self, ix: usize, window: &mut Window, cx: &mut Context<Self>) {
2102        maybe!({
2103            let pane = cx.entity().clone();
2104            let destination_index = self.pinned_tab_count.min(ix);
2105            self.pinned_tab_count += 1;
2106            let id = self.item_for_index(ix)?.item_id();
2107
2108            if self.is_active_preview_item(id) {
2109                self.set_preview_item_id(None, cx);
2110            }
2111
2112            if ix == destination_index {
2113                cx.notify();
2114            } else {
2115                self.workspace
2116                    .update(cx, |_, cx| {
2117                        cx.defer_in(window, move |_, window, cx| {
2118                            move_item(&pane, &pane, id, destination_index, window, cx)
2119                        });
2120                    })
2121                    .ok()?;
2122            }
2123            cx.emit(Event::ItemPinned);
2124
2125            Some(())
2126        });
2127    }
2128
2129    fn unpin_tab_at(&mut self, ix: usize, window: &mut Window, cx: &mut Context<Self>) {
2130        maybe!({
2131            let pane = cx.entity().clone();
2132            self.pinned_tab_count = self.pinned_tab_count.checked_sub(1)?;
2133            let destination_index = self.pinned_tab_count;
2134
2135            let id = self.item_for_index(ix)?.item_id();
2136
2137            if ix == destination_index {
2138                cx.notify()
2139            } else {
2140                self.workspace
2141                    .update(cx, |_, cx| {
2142                        cx.defer_in(window, move |_, window, cx| {
2143                            move_item(&pane, &pane, id, destination_index, window, cx)
2144                        });
2145                    })
2146                    .ok()?;
2147            }
2148            cx.emit(Event::ItemUnpinned);
2149
2150            Some(())
2151        });
2152    }
2153
2154    fn is_tab_pinned(&self, ix: usize) -> bool {
2155        self.pinned_tab_count > ix
2156    }
2157
2158    fn has_pinned_tabs(&self) -> bool {
2159        self.pinned_tab_count != 0
2160    }
2161
2162    fn has_unpinned_tabs(&self) -> bool {
2163        self.pinned_tab_count < self.items.len()
2164    }
2165
2166    fn activate_unpinned_tab(&mut self, window: &mut Window, cx: &mut Context<Self>) {
2167        if self.items.is_empty() {
2168            return;
2169        }
2170        let Some(index) = self
2171            .items()
2172            .enumerate()
2173            .find_map(|(index, _item)| (!self.is_tab_pinned(index)).then_some(index))
2174        else {
2175            return;
2176        };
2177        self.activate_item(index, true, true, window, cx);
2178    }
2179
2180    fn render_tab(
2181        &self,
2182        ix: usize,
2183        item: &dyn ItemHandle,
2184        detail: usize,
2185        focus_handle: &FocusHandle,
2186        window: &mut Window,
2187        cx: &mut Context<Pane>,
2188    ) -> impl IntoElement + use<> {
2189        let is_active = ix == self.active_item_index;
2190        let is_preview = self
2191            .preview_item_id
2192            .map(|id| id == item.item_id())
2193            .unwrap_or(false);
2194
2195        let label = item.tab_content(
2196            TabContentParams {
2197                detail: Some(detail),
2198                selected: is_active,
2199                preview: is_preview,
2200                deemphasized: !self.has_focus(window, cx),
2201            },
2202            window,
2203            cx,
2204        );
2205
2206        let item_diagnostic = item
2207            .project_path(cx)
2208            .map_or(None, |project_path| self.diagnostics.get(&project_path));
2209
2210        let decorated_icon = item_diagnostic.map_or(None, |diagnostic| {
2211            let icon = match item.tab_icon(window, cx) {
2212                Some(icon) => icon,
2213                None => return None,
2214            };
2215
2216            let knockout_item_color = if is_active {
2217                cx.theme().colors().tab_active_background
2218            } else {
2219                cx.theme().colors().tab_bar_background
2220            };
2221
2222            let (icon_decoration, icon_color) = if matches!(diagnostic, &DiagnosticSeverity::ERROR)
2223            {
2224                (IconDecorationKind::X, Color::Error)
2225            } else {
2226                (IconDecorationKind::Triangle, Color::Warning)
2227            };
2228
2229            Some(DecoratedIcon::new(
2230                icon.size(IconSize::Small).color(Color::Muted),
2231                Some(
2232                    IconDecoration::new(icon_decoration, knockout_item_color, cx)
2233                        .color(icon_color.color(cx))
2234                        .position(Point {
2235                            x: px(-2.),
2236                            y: px(-2.),
2237                        }),
2238                ),
2239            ))
2240        });
2241
2242        let icon = if decorated_icon.is_none() {
2243            match item_diagnostic {
2244                Some(&DiagnosticSeverity::ERROR) => None,
2245                Some(&DiagnosticSeverity::WARNING) => None,
2246                _ => item
2247                    .tab_icon(window, cx)
2248                    .map(|icon| icon.color(Color::Muted)),
2249            }
2250            .map(|icon| icon.size(IconSize::Small))
2251        } else {
2252            None
2253        };
2254
2255        let settings = ItemSettings::get_global(cx);
2256        let close_side = &settings.close_position;
2257        let show_close_button = &settings.show_close_button;
2258        let indicator = render_item_indicator(item.boxed_clone(), cx);
2259        let item_id = item.item_id();
2260        let is_first_item = ix == 0;
2261        let is_last_item = ix == self.items.len() - 1;
2262        let is_pinned = self.is_tab_pinned(ix);
2263        let position_relative_to_active_item = ix.cmp(&self.active_item_index);
2264
2265        let tab = Tab::new(ix)
2266            .position(if is_first_item {
2267                TabPosition::First
2268            } else if is_last_item {
2269                TabPosition::Last
2270            } else {
2271                TabPosition::Middle(position_relative_to_active_item)
2272            })
2273            .close_side(match close_side {
2274                ClosePosition::Left => ui::TabCloseSide::Start,
2275                ClosePosition::Right => ui::TabCloseSide::End,
2276            })
2277            .toggle_state(is_active)
2278            .on_click(cx.listener(move |pane: &mut Self, _, window, cx| {
2279                pane.activate_item(ix, true, true, window, cx)
2280            }))
2281            // TODO: This should be a click listener with the middle mouse button instead of a mouse down listener.
2282            .on_mouse_down(
2283                MouseButton::Middle,
2284                cx.listener(move |pane, _event, window, cx| {
2285                    pane.close_item_by_id(item_id, SaveIntent::Close, window, cx)
2286                        .detach_and_log_err(cx);
2287                }),
2288            )
2289            .on_mouse_down(
2290                MouseButton::Left,
2291                cx.listener(move |pane, event: &MouseDownEvent, _, cx| {
2292                    if let Some(id) = pane.preview_item_id {
2293                        if id == item_id && event.click_count > 1 {
2294                            pane.set_preview_item_id(None, cx);
2295                        }
2296                    }
2297                }),
2298            )
2299            .on_drag(
2300                DraggedTab {
2301                    item: item.boxed_clone(),
2302                    pane: cx.entity().clone(),
2303                    detail,
2304                    is_active,
2305                    ix,
2306                },
2307                |tab, _, _, cx| cx.new(|_| tab.clone()),
2308            )
2309            .drag_over::<DraggedTab>(|tab, _, _, cx| {
2310                tab.bg(cx.theme().colors().drop_target_background)
2311            })
2312            .drag_over::<DraggedSelection>(|tab, _, _, cx| {
2313                tab.bg(cx.theme().colors().drop_target_background)
2314            })
2315            .when_some(self.can_drop_predicate.clone(), |this, p| {
2316                this.can_drop(move |a, window, cx| p(a, window, cx))
2317            })
2318            .on_drop(
2319                cx.listener(move |this, dragged_tab: &DraggedTab, window, cx| {
2320                    this.drag_split_direction = None;
2321                    this.handle_tab_drop(dragged_tab, ix, window, cx)
2322                }),
2323            )
2324            .on_drop(
2325                cx.listener(move |this, selection: &DraggedSelection, window, cx| {
2326                    this.drag_split_direction = None;
2327                    this.handle_dragged_selection_drop(selection, Some(ix), window, cx)
2328                }),
2329            )
2330            .on_drop(cx.listener(move |this, paths, window, cx| {
2331                this.drag_split_direction = None;
2332                this.handle_external_paths_drop(paths, window, cx)
2333            }))
2334            .when_some(item.tab_tooltip_content(cx), |tab, content| match content {
2335                TabTooltipContent::Text(text) => tab.tooltip(Tooltip::text(text.clone())),
2336                TabTooltipContent::Custom(element_fn) => {
2337                    tab.tooltip(move |window, cx| element_fn(window, cx))
2338                }
2339            })
2340            .start_slot::<Indicator>(indicator)
2341            .map(|this| {
2342                let end_slot_action: &'static dyn Action;
2343                let end_slot_tooltip_text: &'static str;
2344                let end_slot = if is_pinned {
2345                    end_slot_action = &TogglePinTab;
2346                    end_slot_tooltip_text = "Unpin Tab";
2347                    IconButton::new("unpin tab", IconName::Pin)
2348                        .shape(IconButtonShape::Square)
2349                        .icon_color(Color::Muted)
2350                        .size(ButtonSize::None)
2351                        .icon_size(IconSize::XSmall)
2352                        .on_click(cx.listener(move |pane, _, window, cx| {
2353                            pane.unpin_tab_at(ix, window, cx);
2354                        }))
2355                } else {
2356                    end_slot_action = &CloseActiveItem {
2357                        save_intent: None,
2358                        close_pinned: false,
2359                    };
2360                    end_slot_tooltip_text = "Close Tab";
2361                    match show_close_button {
2362                        ShowCloseButton::Always => IconButton::new("close tab", IconName::Close),
2363                        ShowCloseButton::Hover => {
2364                            IconButton::new("close tab", IconName::Close).visible_on_hover("")
2365                        }
2366                        ShowCloseButton::Hidden => return this,
2367                    }
2368                    .shape(IconButtonShape::Square)
2369                    .icon_color(Color::Muted)
2370                    .size(ButtonSize::None)
2371                    .icon_size(IconSize::XSmall)
2372                    .on_click(cx.listener(move |pane, _, window, cx| {
2373                        pane.close_item_by_id(item_id, SaveIntent::Close, window, cx)
2374                            .detach_and_log_err(cx);
2375                    }))
2376                }
2377                .map(|this| {
2378                    if is_active {
2379                        let focus_handle = focus_handle.clone();
2380                        this.tooltip(move |window, cx| {
2381                            Tooltip::for_action_in(
2382                                end_slot_tooltip_text,
2383                                end_slot_action,
2384                                &focus_handle,
2385                                window,
2386                                cx,
2387                            )
2388                        })
2389                    } else {
2390                        this.tooltip(Tooltip::text(end_slot_tooltip_text))
2391                    }
2392                });
2393                this.end_slot(end_slot)
2394            })
2395            .child(
2396                h_flex()
2397                    .gap_1()
2398                    .items_center()
2399                    .children(
2400                        std::iter::once(if let Some(decorated_icon) = decorated_icon {
2401                            Some(div().child(decorated_icon.into_any_element()))
2402                        } else if let Some(icon) = icon {
2403                            Some(div().child(icon.into_any_element()))
2404                        } else {
2405                            None
2406                        })
2407                        .flatten(),
2408                    )
2409                    .child(label),
2410            );
2411
2412        let single_entry_to_resolve = self.items[ix]
2413            .is_singleton(cx)
2414            .then(|| self.items[ix].project_entry_ids(cx).get(0).copied())
2415            .flatten();
2416
2417        let total_items = self.items.len();
2418        let has_items_to_left = ix > 0;
2419        let has_items_to_right = ix < total_items - 1;
2420        let has_clean_items = self.items.iter().any(|item| !item.is_dirty(cx));
2421        let is_pinned = self.is_tab_pinned(ix);
2422        let pane = cx.entity().downgrade();
2423        let menu_context = item.item_focus_handle(cx);
2424        right_click_menu(ix)
2425            .trigger(|_| tab)
2426            .menu(move |window, cx| {
2427                let pane = pane.clone();
2428                let menu_context = menu_context.clone();
2429                ContextMenu::build(window, cx, move |mut menu, window, cx| {
2430                    let close_active_item_action = CloseActiveItem {
2431                        save_intent: None,
2432                        close_pinned: true,
2433                    };
2434                    let close_inactive_items_action = CloseInactiveItems {
2435                        save_intent: None,
2436                        close_pinned: false,
2437                    };
2438                    let close_items_to_the_left_action = CloseItemsToTheLeft {
2439                        close_pinned: false,
2440                    };
2441                    let close_items_to_the_right_action = CloseItemsToTheRight {
2442                        close_pinned: false,
2443                    };
2444                    let close_clean_items_action = CloseCleanItems {
2445                        close_pinned: false,
2446                    };
2447                    let close_all_items_action = CloseAllItems {
2448                        save_intent: None,
2449                        close_pinned: false,
2450                    };
2451                    if let Some(pane) = pane.upgrade() {
2452                        menu = menu
2453                            .entry(
2454                                "Close",
2455                                Some(Box::new(close_active_item_action)),
2456                                window.handler_for(&pane, move |pane, window, cx| {
2457                                    pane.close_item_by_id(item_id, SaveIntent::Close, window, cx)
2458                                        .detach_and_log_err(cx);
2459                                }),
2460                            )
2461                            .item(ContextMenuItem::Entry(
2462                                ContextMenuEntry::new("Close Others")
2463                                    .action(Box::new(close_inactive_items_action.clone()))
2464                                    .disabled(total_items == 1)
2465                                    .handler(window.handler_for(&pane, move |pane, window, cx| {
2466                                        pane.close_inactive_items(
2467                                            &close_inactive_items_action,
2468                                            window,
2469                                            cx,
2470                                        )
2471                                        .detach_and_log_err(cx);
2472                                    })),
2473                            ))
2474                            .separator()
2475                            .item(ContextMenuItem::Entry(
2476                                ContextMenuEntry::new("Close Left")
2477                                    .action(Box::new(close_items_to_the_left_action.clone()))
2478                                    .disabled(!has_items_to_left)
2479                                    .handler(window.handler_for(&pane, move |pane, window, cx| {
2480                                        pane.close_items_to_the_left_by_id(
2481                                            Some(item_id),
2482                                            &close_items_to_the_left_action,
2483                                            window,
2484                                            cx,
2485                                        )
2486                                        .detach_and_log_err(cx);
2487                                    })),
2488                            ))
2489                            .item(ContextMenuItem::Entry(
2490                                ContextMenuEntry::new("Close Right")
2491                                    .action(Box::new(close_items_to_the_right_action.clone()))
2492                                    .disabled(!has_items_to_right)
2493                                    .handler(window.handler_for(&pane, move |pane, window, cx| {
2494                                        pane.close_items_to_the_right_by_id(
2495                                            Some(item_id),
2496                                            &close_items_to_the_right_action,
2497                                            window,
2498                                            cx,
2499                                        )
2500                                        .detach_and_log_err(cx);
2501                                    })),
2502                            ))
2503                            .separator()
2504                            .item(ContextMenuItem::Entry(
2505                                ContextMenuEntry::new("Close Clean")
2506                                    .action(Box::new(close_clean_items_action.clone()))
2507                                    .disabled(!has_clean_items)
2508                                    .handler(window.handler_for(&pane, move |pane, window, cx| {
2509                                        pane.close_clean_items(
2510                                            &close_clean_items_action,
2511                                            window,
2512                                            cx,
2513                                        )
2514                                        .detach_and_log_err(cx)
2515                                    })),
2516                            ))
2517                            .entry(
2518                                "Close All",
2519                                Some(Box::new(close_all_items_action.clone())),
2520                                window.handler_for(&pane, move |pane, window, cx| {
2521                                    pane.close_all_items(&close_all_items_action, window, cx)
2522                                        .detach_and_log_err(cx)
2523                                }),
2524                            );
2525
2526                        let pin_tab_entries = |menu: ContextMenu| {
2527                            menu.separator().map(|this| {
2528                                if is_pinned {
2529                                    this.entry(
2530                                        "Unpin Tab",
2531                                        Some(TogglePinTab.boxed_clone()),
2532                                        window.handler_for(&pane, move |pane, window, cx| {
2533                                            pane.unpin_tab_at(ix, window, cx);
2534                                        }),
2535                                    )
2536                                } else {
2537                                    this.entry(
2538                                        "Pin Tab",
2539                                        Some(TogglePinTab.boxed_clone()),
2540                                        window.handler_for(&pane, move |pane, window, cx| {
2541                                            pane.pin_tab_at(ix, window, cx);
2542                                        }),
2543                                    )
2544                                }
2545                            })
2546                        };
2547                        if let Some(entry) = single_entry_to_resolve {
2548                            let project_path = pane
2549                                .read(cx)
2550                                .item_for_entry(entry, cx)
2551                                .and_then(|item| item.project_path(cx));
2552                            let worktree = project_path.as_ref().and_then(|project_path| {
2553                                pane.read(cx)
2554                                    .project
2555                                    .upgrade()?
2556                                    .read(cx)
2557                                    .worktree_for_id(project_path.worktree_id, cx)
2558                            });
2559                            let has_relative_path = worktree.as_ref().is_some_and(|worktree| {
2560                                worktree
2561                                    .read(cx)
2562                                    .root_entry()
2563                                    .map_or(false, |entry| entry.is_dir())
2564                            });
2565
2566                            let entry_abs_path = pane.read(cx).entry_abs_path(entry, cx);
2567                            let parent_abs_path = entry_abs_path
2568                                .as_deref()
2569                                .and_then(|abs_path| Some(abs_path.parent()?.to_path_buf()));
2570                            let relative_path = project_path
2571                                .map(|project_path| project_path.path)
2572                                .filter(|_| has_relative_path);
2573
2574                            let visible_in_project_panel = relative_path.is_some()
2575                                && worktree.is_some_and(|worktree| worktree.read(cx).is_visible());
2576
2577                            let entry_id = entry.to_proto();
2578                            menu = menu
2579                                .separator()
2580                                .when_some(entry_abs_path, |menu, abs_path| {
2581                                    menu.entry(
2582                                        "Copy Path",
2583                                        Some(Box::new(zed_actions::workspace::CopyPath)),
2584                                        window.handler_for(&pane, move |_, _, cx| {
2585                                            cx.write_to_clipboard(ClipboardItem::new_string(
2586                                                abs_path.to_string_lossy().to_string(),
2587                                            ));
2588                                        }),
2589                                    )
2590                                })
2591                                .when_some(relative_path, |menu, relative_path| {
2592                                    menu.entry(
2593                                        "Copy Relative Path",
2594                                        Some(Box::new(zed_actions::workspace::CopyRelativePath)),
2595                                        window.handler_for(&pane, move |_, _, cx| {
2596                                            cx.write_to_clipboard(ClipboardItem::new_string(
2597                                                relative_path.to_string_lossy().to_string(),
2598                                            ));
2599                                        }),
2600                                    )
2601                                })
2602                                .map(pin_tab_entries)
2603                                .separator()
2604                                .when(visible_in_project_panel, |menu| {
2605                                    menu.entry(
2606                                        "Reveal In Project Panel",
2607                                        Some(Box::new(RevealInProjectPanel {
2608                                            entry_id: Some(entry_id),
2609                                        })),
2610                                        window.handler_for(&pane, move |pane, _, cx| {
2611                                            pane.project
2612                                                .update(cx, |_, cx| {
2613                                                    cx.emit(project::Event::RevealInProjectPanel(
2614                                                        ProjectEntryId::from_proto(entry_id),
2615                                                    ))
2616                                                })
2617                                                .ok();
2618                                        }),
2619                                    )
2620                                })
2621                                .when_some(parent_abs_path, |menu, parent_abs_path| {
2622                                    menu.entry(
2623                                        "Open in Terminal",
2624                                        Some(Box::new(OpenInTerminal)),
2625                                        window.handler_for(&pane, move |_, window, cx| {
2626                                            window.dispatch_action(
2627                                                OpenTerminal {
2628                                                    working_directory: parent_abs_path.clone(),
2629                                                }
2630                                                .boxed_clone(),
2631                                                cx,
2632                                            );
2633                                        }),
2634                                    )
2635                                });
2636                        } else {
2637                            menu = menu.map(pin_tab_entries);
2638                        }
2639                    }
2640
2641                    menu.context(menu_context)
2642                })
2643            })
2644    }
2645
2646    fn render_tab_bar(&mut self, window: &mut Window, cx: &mut Context<Pane>) -> AnyElement {
2647        let focus_handle = self.focus_handle.clone();
2648        let navigate_backward = IconButton::new("navigate_backward", IconName::ArrowLeft)
2649            .icon_size(IconSize::Small)
2650            .on_click({
2651                let entity = cx.entity().clone();
2652                move |_, window, cx| {
2653                    entity.update(cx, |pane, cx| pane.navigate_backward(window, cx))
2654                }
2655            })
2656            .disabled(!self.can_navigate_backward())
2657            .tooltip({
2658                let focus_handle = focus_handle.clone();
2659                move |window, cx| {
2660                    Tooltip::for_action_in("Go Back", &GoBack, &focus_handle, window, cx)
2661                }
2662            });
2663
2664        let navigate_forward = IconButton::new("navigate_forward", IconName::ArrowRight)
2665            .icon_size(IconSize::Small)
2666            .on_click({
2667                let entity = cx.entity().clone();
2668                move |_, window, cx| entity.update(cx, |pane, cx| pane.navigate_forward(window, cx))
2669            })
2670            .disabled(!self.can_navigate_forward())
2671            .tooltip({
2672                let focus_handle = focus_handle.clone();
2673                move |window, cx| {
2674                    Tooltip::for_action_in("Go Forward", &GoForward, &focus_handle, window, cx)
2675                }
2676            });
2677
2678        let mut tab_items = self
2679            .items
2680            .iter()
2681            .enumerate()
2682            .zip(tab_details(&self.items, window, cx))
2683            .map(|((ix, item), detail)| {
2684                self.render_tab(ix, &**item, detail, &focus_handle, window, cx)
2685            })
2686            .collect::<Vec<_>>();
2687        let tab_count = tab_items.len();
2688        let unpinned_tabs = tab_items.split_off(self.pinned_tab_count);
2689        let pinned_tabs = tab_items;
2690        TabBar::new("tab_bar")
2691            .when(
2692                self.display_nav_history_buttons.unwrap_or_default(),
2693                |tab_bar| {
2694                    tab_bar
2695                        .start_child(navigate_backward)
2696                        .start_child(navigate_forward)
2697                },
2698            )
2699            .map(|tab_bar| {
2700                if self.show_tab_bar_buttons {
2701                    let render_tab_buttons = self.render_tab_bar_buttons.clone();
2702                    let (left_children, right_children) = render_tab_buttons(self, window, cx);
2703                    tab_bar
2704                        .start_children(left_children)
2705                        .end_children(right_children)
2706                } else {
2707                    tab_bar
2708                }
2709            })
2710            .children(pinned_tabs.len().ne(&0).then(|| {
2711                let content_width = self.tab_bar_scroll_handle.content_size().width;
2712                let viewport_width = self.tab_bar_scroll_handle.viewport().size.width;
2713                // We need to check both because offset returns delta values even when the scroll handle is not scrollable
2714                let is_scrollable = content_width > viewport_width;
2715                let is_scrolled = self.tab_bar_scroll_handle.offset().x < px(0.);
2716                let has_active_unpinned_tab = self.active_item_index >= self.pinned_tab_count;
2717                h_flex()
2718                    .children(pinned_tabs)
2719                    .when(is_scrollable && is_scrolled, |this| {
2720                        this.when(has_active_unpinned_tab, |this| this.border_r_2())
2721                            .when(!has_active_unpinned_tab, |this| this.border_r_1())
2722                            .border_color(cx.theme().colors().border)
2723                    })
2724            }))
2725            .child(
2726                h_flex()
2727                    .id("unpinned tabs")
2728                    .overflow_x_scroll()
2729                    .w_full()
2730                    .track_scroll(&self.tab_bar_scroll_handle)
2731                    .children(unpinned_tabs)
2732                    .child(
2733                        div()
2734                            .id("tab_bar_drop_target")
2735                            .min_w_6()
2736                            // HACK: This empty child is currently necessary to force the drop target to appear
2737                            // despite us setting a min width above.
2738                            .child("")
2739                            .h_full()
2740                            .flex_grow()
2741                            .drag_over::<DraggedTab>(|bar, _, _, cx| {
2742                                bar.bg(cx.theme().colors().drop_target_background)
2743                            })
2744                            .drag_over::<DraggedSelection>(|bar, _, _, cx| {
2745                                bar.bg(cx.theme().colors().drop_target_background)
2746                            })
2747                            .on_drop(cx.listener(
2748                                move |this, dragged_tab: &DraggedTab, window, cx| {
2749                                    this.drag_split_direction = None;
2750                                    this.handle_tab_drop(dragged_tab, this.items.len(), window, cx)
2751                                },
2752                            ))
2753                            .on_drop(cx.listener(
2754                                move |this, selection: &DraggedSelection, window, cx| {
2755                                    this.drag_split_direction = None;
2756                                    this.handle_project_entry_drop(
2757                                        &selection.active_selection.entry_id,
2758                                        Some(tab_count),
2759                                        window,
2760                                        cx,
2761                                    )
2762                                },
2763                            ))
2764                            .on_drop(cx.listener(move |this, paths, window, cx| {
2765                                this.drag_split_direction = None;
2766                                this.handle_external_paths_drop(paths, window, cx)
2767                            }))
2768                            .on_click(cx.listener(move |this, event: &ClickEvent, window, cx| {
2769                                if event.up.click_count == 2 {
2770                                    window.dispatch_action(
2771                                        this.double_click_dispatch_action.boxed_clone(),
2772                                        cx,
2773                                    );
2774                                }
2775                            })),
2776                    ),
2777            )
2778            .into_any_element()
2779    }
2780
2781    pub fn render_menu_overlay(menu: &Entity<ContextMenu>) -> Div {
2782        div().absolute().bottom_0().right_0().size_0().child(
2783            deferred(anchored().anchor(Corner::TopRight).child(menu.clone())).with_priority(1),
2784        )
2785    }
2786
2787    pub fn set_zoomed(&mut self, zoomed: bool, cx: &mut Context<Self>) {
2788        self.zoomed = zoomed;
2789        cx.notify();
2790    }
2791
2792    pub fn is_zoomed(&self) -> bool {
2793        self.zoomed
2794    }
2795
2796    fn handle_drag_move<T: 'static>(
2797        &mut self,
2798        event: &DragMoveEvent<T>,
2799        window: &mut Window,
2800        cx: &mut Context<Self>,
2801    ) {
2802        let can_split_predicate = self.can_split_predicate.take();
2803        let can_split = match &can_split_predicate {
2804            Some(can_split_predicate) => {
2805                can_split_predicate(self, event.dragged_item(), window, cx)
2806            }
2807            None => false,
2808        };
2809        self.can_split_predicate = can_split_predicate;
2810        if !can_split {
2811            return;
2812        }
2813
2814        let rect = event.bounds.size;
2815
2816        let size = event.bounds.size.width.min(event.bounds.size.height)
2817            * WorkspaceSettings::get_global(cx).drop_target_size;
2818
2819        let relative_cursor = Point::new(
2820            event.event.position.x - event.bounds.left(),
2821            event.event.position.y - event.bounds.top(),
2822        );
2823
2824        let direction = if relative_cursor.x < size
2825            || relative_cursor.x > rect.width - size
2826            || relative_cursor.y < size
2827            || relative_cursor.y > rect.height - size
2828        {
2829            [
2830                SplitDirection::Up,
2831                SplitDirection::Right,
2832                SplitDirection::Down,
2833                SplitDirection::Left,
2834            ]
2835            .iter()
2836            .min_by_key(|side| match side {
2837                SplitDirection::Up => relative_cursor.y,
2838                SplitDirection::Right => rect.width - relative_cursor.x,
2839                SplitDirection::Down => rect.height - relative_cursor.y,
2840                SplitDirection::Left => relative_cursor.x,
2841            })
2842            .cloned()
2843        } else {
2844            None
2845        };
2846
2847        if direction != self.drag_split_direction {
2848            self.drag_split_direction = direction;
2849        }
2850    }
2851
2852    pub fn handle_tab_drop(
2853        &mut self,
2854        dragged_tab: &DraggedTab,
2855        ix: usize,
2856        window: &mut Window,
2857        cx: &mut Context<Self>,
2858    ) {
2859        if let Some(custom_drop_handle) = self.custom_drop_handle.clone() {
2860            if let ControlFlow::Break(()) = custom_drop_handle(self, dragged_tab, window, cx) {
2861                return;
2862            }
2863        }
2864        let mut to_pane = cx.entity().clone();
2865        let split_direction = self.drag_split_direction;
2866        let item_id = dragged_tab.item.item_id();
2867        if let Some(preview_item_id) = self.preview_item_id {
2868            if item_id == preview_item_id {
2869                self.set_preview_item_id(None, cx);
2870            }
2871        }
2872
2873        let is_clone = cfg!(target_os = "macos") && window.modifiers().alt
2874            || cfg!(not(target_os = "macos")) && window.modifiers().control;
2875
2876        let from_pane = dragged_tab.pane.clone();
2877        self.workspace
2878            .update(cx, |_, cx| {
2879                cx.defer_in(window, move |workspace, window, cx| {
2880                    if let Some(split_direction) = split_direction {
2881                        to_pane = workspace.split_pane(to_pane, split_direction, window, cx);
2882                    }
2883                    let database_id = workspace.database_id();
2884                    let old_ix = from_pane.read(cx).index_for_item_id(item_id);
2885                    let old_len = to_pane.read(cx).items.len();
2886                    if is_clone {
2887                        let Some(item) = from_pane
2888                            .read(cx)
2889                            .items()
2890                            .find(|item| item.item_id() == item_id)
2891                            .map(|item| item.clone())
2892                        else {
2893                            return;
2894                        };
2895                        if let Some(item) = item.clone_on_split(database_id, window, cx) {
2896                            to_pane.update(cx, |pane, cx| {
2897                                pane.add_item(item, true, true, None, window, cx);
2898                            })
2899                        }
2900                    } else {
2901                        move_item(&from_pane, &to_pane, item_id, ix, window, cx);
2902                    }
2903                    if to_pane == from_pane {
2904                        if let Some(old_index) = old_ix {
2905                            to_pane.update(cx, |this, _| {
2906                                if old_index < this.pinned_tab_count
2907                                    && (ix == this.items.len() || ix > this.pinned_tab_count)
2908                                {
2909                                    this.pinned_tab_count -= 1;
2910                                } else if this.has_pinned_tabs()
2911                                    && old_index >= this.pinned_tab_count
2912                                    && ix < this.pinned_tab_count
2913                                {
2914                                    this.pinned_tab_count += 1;
2915                                }
2916                            });
2917                        }
2918                    } else {
2919                        to_pane.update(cx, |this, _| {
2920                            if this.items.len() > old_len // Did we not deduplicate on drag?
2921                                && this.has_pinned_tabs()
2922                                && ix < this.pinned_tab_count
2923                            {
2924                                this.pinned_tab_count += 1;
2925                            }
2926                        });
2927                        from_pane.update(cx, |this, _| {
2928                            if let Some(index) = old_ix {
2929                                if this.pinned_tab_count > index {
2930                                    this.pinned_tab_count -= 1;
2931                                }
2932                            }
2933                        })
2934                    }
2935                });
2936            })
2937            .log_err();
2938    }
2939
2940    fn handle_dragged_selection_drop(
2941        &mut self,
2942        dragged_selection: &DraggedSelection,
2943        dragged_onto: Option<usize>,
2944        window: &mut Window,
2945        cx: &mut Context<Self>,
2946    ) {
2947        if let Some(custom_drop_handle) = self.custom_drop_handle.clone() {
2948            if let ControlFlow::Break(()) = custom_drop_handle(self, dragged_selection, window, cx)
2949            {
2950                return;
2951            }
2952        }
2953        self.handle_project_entry_drop(
2954            &dragged_selection.active_selection.entry_id,
2955            dragged_onto,
2956            window,
2957            cx,
2958        );
2959    }
2960
2961    fn handle_project_entry_drop(
2962        &mut self,
2963        project_entry_id: &ProjectEntryId,
2964        target: Option<usize>,
2965        window: &mut Window,
2966        cx: &mut Context<Self>,
2967    ) {
2968        if let Some(custom_drop_handle) = self.custom_drop_handle.clone() {
2969            if let ControlFlow::Break(()) = custom_drop_handle(self, project_entry_id, window, cx) {
2970                return;
2971            }
2972        }
2973        let mut to_pane = cx.entity().clone();
2974        let split_direction = self.drag_split_direction;
2975        let project_entry_id = *project_entry_id;
2976        self.workspace
2977            .update(cx, |_, cx| {
2978                cx.defer_in(window, move |workspace, window, cx| {
2979                    if let Some(project_path) = workspace
2980                        .project()
2981                        .read(cx)
2982                        .path_for_entry(project_entry_id, cx)
2983                    {
2984                        let load_path_task = workspace.load_path(project_path.clone(), window, cx);
2985                        cx.spawn_in(window, async move |workspace, cx| {
2986                            if let Some((project_entry_id, build_item)) =
2987                                load_path_task.await.notify_async_err(cx)
2988                            {
2989                                let (to_pane, new_item_handle) = workspace
2990                                    .update_in(cx, |workspace, window, cx| {
2991                                        if let Some(split_direction) = split_direction {
2992                                            to_pane = workspace.split_pane(
2993                                                to_pane,
2994                                                split_direction,
2995                                                window,
2996                                                cx,
2997                                            );
2998                                        }
2999                                        let new_item_handle = to_pane.update(cx, |pane, cx| {
3000                                            pane.open_item(
3001                                                project_entry_id,
3002                                                project_path,
3003                                                true,
3004                                                false,
3005                                                true,
3006                                                target,
3007                                                window,
3008                                                cx,
3009                                                build_item,
3010                                            )
3011                                        });
3012                                        (to_pane, new_item_handle)
3013                                    })
3014                                    .log_err()?;
3015                                to_pane
3016                                    .update_in(cx, |this, window, cx| {
3017                                        let Some(index) = this.index_for_item(&*new_item_handle)
3018                                        else {
3019                                            return;
3020                                        };
3021
3022                                        if target.map_or(false, |target| this.is_tab_pinned(target))
3023                                        {
3024                                            this.pin_tab_at(index, window, cx);
3025                                        }
3026                                    })
3027                                    .ok()?
3028                            }
3029                            Some(())
3030                        })
3031                        .detach();
3032                    };
3033                });
3034            })
3035            .log_err();
3036    }
3037
3038    fn handle_external_paths_drop(
3039        &mut self,
3040        paths: &ExternalPaths,
3041        window: &mut Window,
3042        cx: &mut Context<Self>,
3043    ) {
3044        if let Some(custom_drop_handle) = self.custom_drop_handle.clone() {
3045            if let ControlFlow::Break(()) = custom_drop_handle(self, paths, window, cx) {
3046                return;
3047            }
3048        }
3049        let mut to_pane = cx.entity().clone();
3050        let mut split_direction = self.drag_split_direction;
3051        let paths = paths.paths().to_vec();
3052        let is_remote = self
3053            .workspace
3054            .update(cx, |workspace, cx| {
3055                if workspace.project().read(cx).is_via_collab() {
3056                    workspace.show_error(
3057                        &anyhow::anyhow!("Cannot drop files on a remote project"),
3058                        cx,
3059                    );
3060                    true
3061                } else {
3062                    false
3063                }
3064            })
3065            .unwrap_or(true);
3066        if is_remote {
3067            return;
3068        }
3069
3070        self.workspace
3071            .update(cx, |workspace, cx| {
3072                let fs = Arc::clone(workspace.project().read(cx).fs());
3073                cx.spawn_in(window, async move |workspace, cx| {
3074                    let mut is_file_checks = FuturesUnordered::new();
3075                    for path in &paths {
3076                        is_file_checks.push(fs.is_file(path))
3077                    }
3078                    let mut has_files_to_open = false;
3079                    while let Some(is_file) = is_file_checks.next().await {
3080                        if is_file {
3081                            has_files_to_open = true;
3082                            break;
3083                        }
3084                    }
3085                    drop(is_file_checks);
3086                    if !has_files_to_open {
3087                        split_direction = None;
3088                    }
3089
3090                    if let Ok(open_task) = workspace.update_in(cx, |workspace, window, cx| {
3091                        if let Some(split_direction) = split_direction {
3092                            to_pane = workspace.split_pane(to_pane, split_direction, window, cx);
3093                        }
3094                        workspace.open_paths(
3095                            paths,
3096                            OpenOptions {
3097                                visible: Some(OpenVisible::OnlyDirectories),
3098                                ..Default::default()
3099                            },
3100                            Some(to_pane.downgrade()),
3101                            window,
3102                            cx,
3103                        )
3104                    }) {
3105                        let opened_items: Vec<_> = open_task.await;
3106                        _ = workspace.update(cx, |workspace, cx| {
3107                            for item in opened_items.into_iter().flatten() {
3108                                if let Err(e) = item {
3109                                    workspace.show_error(&e, cx);
3110                                }
3111                            }
3112                        });
3113                    }
3114                })
3115                .detach();
3116            })
3117            .log_err();
3118    }
3119
3120    pub fn display_nav_history_buttons(&mut self, display: Option<bool>) {
3121        self.display_nav_history_buttons = display;
3122    }
3123
3124    fn pinned_item_ids(&self) -> HashSet<EntityId> {
3125        self.items
3126            .iter()
3127            .enumerate()
3128            .filter_map(|(index, item)| {
3129                if self.is_tab_pinned(index) {
3130                    return Some(item.item_id());
3131                }
3132
3133                None
3134            })
3135            .collect()
3136    }
3137
3138    fn clean_item_ids(&self, cx: &mut Context<Pane>) -> HashSet<EntityId> {
3139        self.items()
3140            .filter_map(|item| {
3141                if !item.is_dirty(cx) {
3142                    return Some(item.item_id());
3143                }
3144
3145                None
3146            })
3147            .collect()
3148    }
3149
3150    fn to_the_side_item_ids(&self, item_id: EntityId, side: Side) -> HashSet<EntityId> {
3151        match side {
3152            Side::Left => self
3153                .items()
3154                .take_while(|item| item.item_id() != item_id)
3155                .map(|item| item.item_id())
3156                .collect(),
3157            Side::Right => self
3158                .items()
3159                .rev()
3160                .take_while(|item| item.item_id() != item_id)
3161                .map(|item| item.item_id())
3162                .collect(),
3163        }
3164    }
3165
3166    pub fn drag_split_direction(&self) -> Option<SplitDirection> {
3167        self.drag_split_direction
3168    }
3169
3170    pub fn set_zoom_out_on_close(&mut self, zoom_out_on_close: bool) {
3171        self.zoom_out_on_close = zoom_out_on_close;
3172    }
3173}
3174
3175fn default_render_tab_bar_buttons(
3176    pane: &mut Pane,
3177    window: &mut Window,
3178    cx: &mut Context<Pane>,
3179) -> (Option<AnyElement>, Option<AnyElement>) {
3180    if !pane.has_focus(window, cx) && !pane.context_menu_focused(window, cx) {
3181        return (None, None);
3182    }
3183    // Ideally we would return a vec of elements here to pass directly to the [TabBar]'s
3184    // `end_slot`, but due to needing a view here that isn't possible.
3185    let right_children = h_flex()
3186        // Instead we need to replicate the spacing from the [TabBar]'s `end_slot` here.
3187        .gap(DynamicSpacing::Base04.rems(cx))
3188        .child(
3189            PopoverMenu::new("pane-tab-bar-popover-menu")
3190                .trigger_with_tooltip(
3191                    IconButton::new("plus", IconName::Plus).icon_size(IconSize::Small),
3192                    Tooltip::text("New..."),
3193                )
3194                .anchor(Corner::TopRight)
3195                .with_handle(pane.new_item_context_menu_handle.clone())
3196                .menu(move |window, cx| {
3197                    Some(ContextMenu::build(window, cx, |menu, _, _| {
3198                        menu.action("New File", NewFile.boxed_clone())
3199                            .action("Open File", ToggleFileFinder::default().boxed_clone())
3200                            .separator()
3201                            .action(
3202                                "Search Project",
3203                                DeploySearch {
3204                                    replace_enabled: false,
3205                                    included_files: None,
3206                                    excluded_files: None,
3207                                }
3208                                .boxed_clone(),
3209                            )
3210                            .action("Search Symbols", ToggleProjectSymbols.boxed_clone())
3211                            .separator()
3212                            .action("New Terminal", NewTerminal.boxed_clone())
3213                    }))
3214                }),
3215        )
3216        .child(
3217            PopoverMenu::new("pane-tab-bar-split")
3218                .trigger_with_tooltip(
3219                    IconButton::new("split", IconName::Split).icon_size(IconSize::Small),
3220                    Tooltip::text("Split Pane"),
3221                )
3222                .anchor(Corner::TopRight)
3223                .with_handle(pane.split_item_context_menu_handle.clone())
3224                .menu(move |window, cx| {
3225                    ContextMenu::build(window, cx, |menu, _, _| {
3226                        menu.action("Split Right", SplitRight.boxed_clone())
3227                            .action("Split Left", SplitLeft.boxed_clone())
3228                            .action("Split Up", SplitUp.boxed_clone())
3229                            .action("Split Down", SplitDown.boxed_clone())
3230                    })
3231                    .into()
3232                }),
3233        )
3234        .child({
3235            let zoomed = pane.is_zoomed();
3236            IconButton::new("toggle_zoom", IconName::Maximize)
3237                .icon_size(IconSize::Small)
3238                .toggle_state(zoomed)
3239                .selected_icon(IconName::Minimize)
3240                .on_click(cx.listener(|pane, _, window, cx| {
3241                    pane.toggle_zoom(&crate::ToggleZoom, window, cx);
3242                }))
3243                .tooltip(move |window, cx| {
3244                    Tooltip::for_action(
3245                        if zoomed { "Zoom Out" } else { "Zoom In" },
3246                        &ToggleZoom,
3247                        window,
3248                        cx,
3249                    )
3250                })
3251        })
3252        .into_any_element()
3253        .into();
3254    (None, right_children)
3255}
3256
3257impl Focusable for Pane {
3258    fn focus_handle(&self, _cx: &App) -> FocusHandle {
3259        self.focus_handle.clone()
3260    }
3261}
3262
3263impl Render for Pane {
3264    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
3265        let mut key_context = KeyContext::new_with_defaults();
3266        key_context.add("Pane");
3267        if self.active_item().is_none() {
3268            key_context.add("EmptyPane");
3269        }
3270
3271        let should_display_tab_bar = self.should_display_tab_bar.clone();
3272        let display_tab_bar = should_display_tab_bar(window, cx);
3273        let Some(project) = self.project.upgrade() else {
3274            return div().track_focus(&self.focus_handle(cx));
3275        };
3276        let is_local = project.read(cx).is_local();
3277
3278        v_flex()
3279            .key_context(key_context)
3280            .track_focus(&self.focus_handle(cx))
3281            .size_full()
3282            .flex_none()
3283            .overflow_hidden()
3284            .on_action(cx.listener(|pane, _: &AlternateFile, window, cx| {
3285                pane.alternate_file(window, cx);
3286            }))
3287            .on_action(
3288                cx.listener(|pane, _: &SplitLeft, _, cx| pane.split(SplitDirection::Left, cx)),
3289            )
3290            .on_action(cx.listener(|pane, _: &SplitUp, _, cx| pane.split(SplitDirection::Up, cx)))
3291            .on_action(cx.listener(|pane, _: &SplitHorizontal, _, cx| {
3292                pane.split(SplitDirection::horizontal(cx), cx)
3293            }))
3294            .on_action(cx.listener(|pane, _: &SplitVertical, _, cx| {
3295                pane.split(SplitDirection::vertical(cx), cx)
3296            }))
3297            .on_action(
3298                cx.listener(|pane, _: &SplitRight, _, cx| pane.split(SplitDirection::Right, cx)),
3299            )
3300            .on_action(
3301                cx.listener(|pane, _: &SplitDown, _, cx| pane.split(SplitDirection::Down, cx)),
3302            )
3303            .on_action(
3304                cx.listener(|pane, _: &GoBack, window, cx| pane.navigate_backward(window, cx)),
3305            )
3306            .on_action(
3307                cx.listener(|pane, _: &GoForward, window, cx| pane.navigate_forward(window, cx)),
3308            )
3309            .on_action(cx.listener(|_, _: &JoinIntoNext, _, cx| {
3310                cx.emit(Event::JoinIntoNext);
3311            }))
3312            .on_action(cx.listener(|_, _: &JoinAll, _, cx| {
3313                cx.emit(Event::JoinAll);
3314            }))
3315            .on_action(cx.listener(Pane::toggle_zoom))
3316            .on_action(
3317                cx.listener(|pane: &mut Pane, action: &ActivateItem, window, cx| {
3318                    pane.activate_item(
3319                        action.0.min(pane.items.len().saturating_sub(1)),
3320                        true,
3321                        true,
3322                        window,
3323                        cx,
3324                    );
3325                }),
3326            )
3327            .on_action(
3328                cx.listener(|pane: &mut Pane, _: &ActivateLastItem, window, cx| {
3329                    pane.activate_item(pane.items.len().saturating_sub(1), true, true, window, cx);
3330                }),
3331            )
3332            .on_action(
3333                cx.listener(|pane: &mut Pane, _: &ActivatePreviousItem, window, cx| {
3334                    pane.activate_prev_item(true, window, cx);
3335                }),
3336            )
3337            .on_action(
3338                cx.listener(|pane: &mut Pane, _: &ActivateNextItem, window, cx| {
3339                    pane.activate_next_item(true, window, cx);
3340                }),
3341            )
3342            .on_action(
3343                cx.listener(|pane, _: &SwapItemLeft, window, cx| pane.swap_item_left(window, cx)),
3344            )
3345            .on_action(
3346                cx.listener(|pane, _: &SwapItemRight, window, cx| pane.swap_item_right(window, cx)),
3347            )
3348            .on_action(cx.listener(|pane, action, window, cx| {
3349                pane.toggle_pin_tab(action, window, cx);
3350            }))
3351            .when(PreviewTabsSettings::get_global(cx).enabled, |this| {
3352                this.on_action(cx.listener(|pane: &mut Pane, _: &TogglePreviewTab, _, cx| {
3353                    if let Some(active_item_id) = pane.active_item().map(|i| i.item_id()) {
3354                        if pane.is_active_preview_item(active_item_id) {
3355                            pane.set_preview_item_id(None, cx);
3356                        } else {
3357                            pane.set_preview_item_id(Some(active_item_id), cx);
3358                        }
3359                    }
3360                }))
3361            })
3362            .on_action(
3363                cx.listener(|pane: &mut Self, action: &CloseActiveItem, window, cx| {
3364                    pane.close_active_item(action, window, cx)
3365                        .detach_and_log_err(cx)
3366                }),
3367            )
3368            .on_action(
3369                cx.listener(|pane: &mut Self, action: &CloseInactiveItems, window, cx| {
3370                    pane.close_inactive_items(action, window, cx)
3371                        .detach_and_log_err(cx);
3372                }),
3373            )
3374            .on_action(
3375                cx.listener(|pane: &mut Self, action: &CloseCleanItems, window, cx| {
3376                    pane.close_clean_items(action, window, cx)
3377                        .detach_and_log_err(cx)
3378                }),
3379            )
3380            .on_action(cx.listener(
3381                |pane: &mut Self, action: &CloseItemsToTheLeft, window, cx| {
3382                    pane.close_items_to_the_left_by_id(None, action, window, cx)
3383                        .detach_and_log_err(cx)
3384                },
3385            ))
3386            .on_action(cx.listener(
3387                |pane: &mut Self, action: &CloseItemsToTheRight, window, cx| {
3388                    pane.close_items_to_the_right_by_id(None, action, window, cx)
3389                        .detach_and_log_err(cx)
3390                },
3391            ))
3392            .on_action(
3393                cx.listener(|pane: &mut Self, action: &CloseAllItems, window, cx| {
3394                    pane.close_all_items(action, window, cx)
3395                        .detach_and_log_err(cx)
3396                }),
3397            )
3398            .on_action(
3399                cx.listener(|pane: &mut Self, action: &RevealInProjectPanel, _, cx| {
3400                    let entry_id = action
3401                        .entry_id
3402                        .map(ProjectEntryId::from_proto)
3403                        .or_else(|| pane.active_item()?.project_entry_ids(cx).first().copied());
3404                    if let Some(entry_id) = entry_id {
3405                        pane.project
3406                            .update(cx, |_, cx| {
3407                                cx.emit(project::Event::RevealInProjectPanel(entry_id))
3408                            })
3409                            .ok();
3410                    }
3411                }),
3412            )
3413            .on_action(cx.listener(|_, _: &menu::Cancel, window, cx| {
3414                if cx.stop_active_drag(window) {
3415                    return;
3416                } else {
3417                    cx.propagate();
3418                }
3419            }))
3420            .when(self.active_item().is_some() && display_tab_bar, |pane| {
3421                pane.child((self.render_tab_bar.clone())(self, window, cx))
3422            })
3423            .child({
3424                let has_worktrees = project.read(cx).visible_worktrees(cx).next().is_some();
3425                // main content
3426                div()
3427                    .flex_1()
3428                    .relative()
3429                    .group("")
3430                    .overflow_hidden()
3431                    .on_drag_move::<DraggedTab>(cx.listener(Self::handle_drag_move))
3432                    .on_drag_move::<DraggedSelection>(cx.listener(Self::handle_drag_move))
3433                    .when(is_local, |div| {
3434                        div.on_drag_move::<ExternalPaths>(cx.listener(Self::handle_drag_move))
3435                    })
3436                    .map(|div| {
3437                        if let Some(item) = self.active_item() {
3438                            div.id("pane_placeholder")
3439                                .v_flex()
3440                                .size_full()
3441                                .overflow_hidden()
3442                                .child(self.toolbar.clone())
3443                                .child(item.to_any())
3444                        } else {
3445                            let placeholder = div
3446                                .id("pane_placeholder")
3447                                .h_flex()
3448                                .size_full()
3449                                .justify_center()
3450                                .on_click(cx.listener(
3451                                    move |this, event: &ClickEvent, window, cx| {
3452                                        if event.up.click_count == 2 {
3453                                            window.dispatch_action(
3454                                                this.double_click_dispatch_action.boxed_clone(),
3455                                                cx,
3456                                            );
3457                                        }
3458                                    },
3459                                ));
3460                            if has_worktrees {
3461                                placeholder
3462                            } else {
3463                                placeholder.child(
3464                                    Label::new("Open a file or project to get started.")
3465                                        .color(Color::Muted),
3466                                )
3467                            }
3468                        }
3469                    })
3470                    .child(
3471                        // drag target
3472                        div()
3473                            .invisible()
3474                            .absolute()
3475                            .bg(cx.theme().colors().drop_target_background)
3476                            .group_drag_over::<DraggedTab>("", |style| style.visible())
3477                            .group_drag_over::<DraggedSelection>("", |style| style.visible())
3478                            .when(is_local, |div| {
3479                                div.group_drag_over::<ExternalPaths>("", |style| style.visible())
3480                            })
3481                            .when_some(self.can_drop_predicate.clone(), |this, p| {
3482                                this.can_drop(move |a, window, cx| p(a, window, cx))
3483                            })
3484                            .on_drop(cx.listener(move |this, dragged_tab, window, cx| {
3485                                this.handle_tab_drop(
3486                                    dragged_tab,
3487                                    this.active_item_index(),
3488                                    window,
3489                                    cx,
3490                                )
3491                            }))
3492                            .on_drop(cx.listener(
3493                                move |this, selection: &DraggedSelection, window, cx| {
3494                                    this.handle_dragged_selection_drop(selection, None, window, cx)
3495                                },
3496                            ))
3497                            .on_drop(cx.listener(move |this, paths, window, cx| {
3498                                this.handle_external_paths_drop(paths, window, cx)
3499                            }))
3500                            .map(|div| {
3501                                let size = DefiniteLength::Fraction(0.5);
3502                                match self.drag_split_direction {
3503                                    None => div.top_0().right_0().bottom_0().left_0(),
3504                                    Some(SplitDirection::Up) => {
3505                                        div.top_0().left_0().right_0().h(size)
3506                                    }
3507                                    Some(SplitDirection::Down) => {
3508                                        div.left_0().bottom_0().right_0().h(size)
3509                                    }
3510                                    Some(SplitDirection::Left) => {
3511                                        div.top_0().left_0().bottom_0().w(size)
3512                                    }
3513                                    Some(SplitDirection::Right) => {
3514                                        div.top_0().bottom_0().right_0().w(size)
3515                                    }
3516                                }
3517                            }),
3518                    )
3519            })
3520            .on_mouse_down(
3521                MouseButton::Navigate(NavigationDirection::Back),
3522                cx.listener(|pane, _, window, cx| {
3523                    if let Some(workspace) = pane.workspace.upgrade() {
3524                        let pane = cx.entity().downgrade();
3525                        window.defer(cx, move |window, cx| {
3526                            workspace.update(cx, |workspace, cx| {
3527                                workspace.go_back(pane, window, cx).detach_and_log_err(cx)
3528                            })
3529                        })
3530                    }
3531                }),
3532            )
3533            .on_mouse_down(
3534                MouseButton::Navigate(NavigationDirection::Forward),
3535                cx.listener(|pane, _, window, cx| {
3536                    if let Some(workspace) = pane.workspace.upgrade() {
3537                        let pane = cx.entity().downgrade();
3538                        window.defer(cx, move |window, cx| {
3539                            workspace.update(cx, |workspace, cx| {
3540                                workspace
3541                                    .go_forward(pane, window, cx)
3542                                    .detach_and_log_err(cx)
3543                            })
3544                        })
3545                    }
3546                }),
3547            )
3548    }
3549}
3550
3551impl ItemNavHistory {
3552    pub fn push<D: 'static + Send + Any>(&mut self, data: Option<D>, cx: &mut App) {
3553        if self
3554            .item
3555            .upgrade()
3556            .is_some_and(|item| item.include_in_nav_history())
3557        {
3558            self.history
3559                .push(data, self.item.clone(), self.is_preview, cx);
3560        }
3561    }
3562
3563    pub fn pop_backward(&mut self, cx: &mut App) -> Option<NavigationEntry> {
3564        self.history.pop(NavigationMode::GoingBack, cx)
3565    }
3566
3567    pub fn pop_forward(&mut self, cx: &mut App) -> Option<NavigationEntry> {
3568        self.history.pop(NavigationMode::GoingForward, cx)
3569    }
3570}
3571
3572impl NavHistory {
3573    pub fn for_each_entry(
3574        &self,
3575        cx: &App,
3576        mut f: impl FnMut(&NavigationEntry, (ProjectPath, Option<PathBuf>)),
3577    ) {
3578        let borrowed_history = self.0.lock();
3579        borrowed_history
3580            .forward_stack
3581            .iter()
3582            .chain(borrowed_history.backward_stack.iter())
3583            .chain(borrowed_history.closed_stack.iter())
3584            .for_each(|entry| {
3585                if let Some(project_and_abs_path) =
3586                    borrowed_history.paths_by_item.get(&entry.item.id())
3587                {
3588                    f(entry, project_and_abs_path.clone());
3589                } else if let Some(item) = entry.item.upgrade() {
3590                    if let Some(path) = item.project_path(cx) {
3591                        f(entry, (path, None));
3592                    }
3593                }
3594            })
3595    }
3596
3597    pub fn set_mode(&mut self, mode: NavigationMode) {
3598        self.0.lock().mode = mode;
3599    }
3600
3601    pub fn mode(&self) -> NavigationMode {
3602        self.0.lock().mode
3603    }
3604
3605    pub fn disable(&mut self) {
3606        self.0.lock().mode = NavigationMode::Disabled;
3607    }
3608
3609    pub fn enable(&mut self) {
3610        self.0.lock().mode = NavigationMode::Normal;
3611    }
3612
3613    pub fn pop(&mut self, mode: NavigationMode, cx: &mut App) -> Option<NavigationEntry> {
3614        let mut state = self.0.lock();
3615        let entry = match mode {
3616            NavigationMode::Normal | NavigationMode::Disabled | NavigationMode::ClosingItem => {
3617                return None;
3618            }
3619            NavigationMode::GoingBack => &mut state.backward_stack,
3620            NavigationMode::GoingForward => &mut state.forward_stack,
3621            NavigationMode::ReopeningClosedItem => &mut state.closed_stack,
3622        }
3623        .pop_back();
3624        if entry.is_some() {
3625            state.did_update(cx);
3626        }
3627        entry
3628    }
3629
3630    pub fn push<D: 'static + Send + Any>(
3631        &mut self,
3632        data: Option<D>,
3633        item: Arc<dyn WeakItemHandle>,
3634        is_preview: bool,
3635        cx: &mut App,
3636    ) {
3637        let state = &mut *self.0.lock();
3638        match state.mode {
3639            NavigationMode::Disabled => {}
3640            NavigationMode::Normal | NavigationMode::ReopeningClosedItem => {
3641                if state.backward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
3642                    state.backward_stack.pop_front();
3643                }
3644                state.backward_stack.push_back(NavigationEntry {
3645                    item,
3646                    data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
3647                    timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
3648                    is_preview,
3649                });
3650                state.forward_stack.clear();
3651            }
3652            NavigationMode::GoingBack => {
3653                if state.forward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
3654                    state.forward_stack.pop_front();
3655                }
3656                state.forward_stack.push_back(NavigationEntry {
3657                    item,
3658                    data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
3659                    timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
3660                    is_preview,
3661                });
3662            }
3663            NavigationMode::GoingForward => {
3664                if state.backward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
3665                    state.backward_stack.pop_front();
3666                }
3667                state.backward_stack.push_back(NavigationEntry {
3668                    item,
3669                    data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
3670                    timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
3671                    is_preview,
3672                });
3673            }
3674            NavigationMode::ClosingItem => {
3675                if state.closed_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
3676                    state.closed_stack.pop_front();
3677                }
3678                state.closed_stack.push_back(NavigationEntry {
3679                    item,
3680                    data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
3681                    timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
3682                    is_preview,
3683                });
3684            }
3685        }
3686        state.did_update(cx);
3687    }
3688
3689    pub fn remove_item(&mut self, item_id: EntityId) {
3690        let mut state = self.0.lock();
3691        state.paths_by_item.remove(&item_id);
3692        state
3693            .backward_stack
3694            .retain(|entry| entry.item.id() != item_id);
3695        state
3696            .forward_stack
3697            .retain(|entry| entry.item.id() != item_id);
3698        state
3699            .closed_stack
3700            .retain(|entry| entry.item.id() != item_id);
3701    }
3702
3703    pub fn path_for_item(&self, item_id: EntityId) -> Option<(ProjectPath, Option<PathBuf>)> {
3704        self.0.lock().paths_by_item.get(&item_id).cloned()
3705    }
3706}
3707
3708impl NavHistoryState {
3709    pub fn did_update(&self, cx: &mut App) {
3710        if let Some(pane) = self.pane.upgrade() {
3711            cx.defer(move |cx| {
3712                pane.update(cx, |pane, cx| pane.history_updated(cx));
3713            });
3714        }
3715    }
3716}
3717
3718fn dirty_message_for(buffer_path: Option<ProjectPath>) -> String {
3719    let path = buffer_path
3720        .as_ref()
3721        .and_then(|p| {
3722            p.path
3723                .to_str()
3724                .and_then(|s| if s.is_empty() { None } else { Some(s) })
3725        })
3726        .unwrap_or("This buffer");
3727    let path = truncate_and_remove_front(path, 80);
3728    format!("{path} contains unsaved edits. Do you want to save it?")
3729}
3730
3731pub fn tab_details(items: &[Box<dyn ItemHandle>], _window: &Window, cx: &App) -> Vec<usize> {
3732    let mut tab_details = items.iter().map(|_| 0).collect::<Vec<_>>();
3733    let mut tab_descriptions = HashMap::default();
3734    let mut done = false;
3735    while !done {
3736        done = true;
3737
3738        // Store item indices by their tab description.
3739        for (ix, (item, detail)) in items.iter().zip(&tab_details).enumerate() {
3740            let description = item.tab_content_text(*detail, cx);
3741            if *detail == 0 || description != item.tab_content_text(detail - 1, cx) {
3742                tab_descriptions
3743                    .entry(description)
3744                    .or_insert(Vec::new())
3745                    .push(ix);
3746            }
3747        }
3748
3749        // If two or more items have the same tab description, increase their level
3750        // of detail and try again.
3751        for (_, item_ixs) in tab_descriptions.drain() {
3752            if item_ixs.len() > 1 {
3753                done = false;
3754                for ix in item_ixs {
3755                    tab_details[ix] += 1;
3756                }
3757            }
3758        }
3759    }
3760
3761    tab_details
3762}
3763
3764pub fn render_item_indicator(item: Box<dyn ItemHandle>, cx: &App) -> Option<Indicator> {
3765    maybe!({
3766        let indicator_color = match (item.has_conflict(cx), item.is_dirty(cx)) {
3767            (true, _) => Color::Warning,
3768            (_, true) => Color::Accent,
3769            (false, false) => return None,
3770        };
3771
3772        Some(Indicator::dot().color(indicator_color))
3773    })
3774}
3775
3776impl Render for DraggedTab {
3777    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
3778        let ui_font = ThemeSettings::get_global(cx).ui_font.clone();
3779        let label = self.item.tab_content(
3780            TabContentParams {
3781                detail: Some(self.detail),
3782                selected: false,
3783                preview: false,
3784                deemphasized: false,
3785            },
3786            window,
3787            cx,
3788        );
3789        Tab::new("")
3790            .toggle_state(self.is_active)
3791            .child(label)
3792            .render(window, cx)
3793            .font(ui_font)
3794    }
3795}
3796
3797#[cfg(test)]
3798mod tests {
3799    use std::num::NonZero;
3800
3801    use super::*;
3802    use crate::item::test::{TestItem, TestProjectItem};
3803    use gpui::{TestAppContext, VisualTestContext};
3804    use project::FakeFs;
3805    use settings::SettingsStore;
3806    use theme::LoadThemes;
3807    use util::TryFutureExt;
3808
3809    #[gpui::test]
3810    async fn test_add_item_capped_to_max_tabs(cx: &mut TestAppContext) {
3811        init_test(cx);
3812        let fs = FakeFs::new(cx.executor());
3813
3814        let project = Project::test(fs, None, cx).await;
3815        let (workspace, cx) =
3816            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
3817        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
3818
3819        for i in 0..7 {
3820            add_labeled_item(&pane, format!("{}", i).as_str(), false, cx);
3821        }
3822        set_max_tabs(cx, Some(5));
3823        add_labeled_item(&pane, "7", false, cx);
3824        // Remove items to respect the max tab cap.
3825        assert_item_labels(&pane, ["3", "4", "5", "6", "7*"], cx);
3826        pane.update_in(cx, |pane, window, cx| {
3827            pane.activate_item(0, false, false, window, cx);
3828        });
3829        add_labeled_item(&pane, "X", false, cx);
3830        // Respect activation order.
3831        assert_item_labels(&pane, ["3", "X*", "5", "6", "7"], cx);
3832
3833        for i in 0..7 {
3834            add_labeled_item(&pane, format!("D{}", i).as_str(), true, cx);
3835        }
3836        // Keeps dirty items, even over max tab cap.
3837        assert_item_labels(
3838            &pane,
3839            ["D0^", "D1^", "D2^", "D3^", "D4^", "D5^", "D6*^"],
3840            cx,
3841        );
3842
3843        set_max_tabs(cx, None);
3844        for i in 0..7 {
3845            add_labeled_item(&pane, format!("N{}", i).as_str(), false, cx);
3846        }
3847        // No cap when max tabs is None.
3848        assert_item_labels(
3849            &pane,
3850            [
3851                "D0^", "D1^", "D2^", "D3^", "D4^", "D5^", "D6^", "N0", "N1", "N2", "N3", "N4",
3852                "N5", "N6*",
3853            ],
3854            cx,
3855        );
3856    }
3857
3858    #[gpui::test]
3859    async fn test_allow_pinning_dirty_item_at_max_tabs(cx: &mut TestAppContext) {
3860        init_test(cx);
3861        let fs = FakeFs::new(cx.executor());
3862
3863        let project = Project::test(fs, None, cx).await;
3864        let (workspace, cx) =
3865            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
3866        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
3867
3868        set_max_tabs(cx, Some(1));
3869        let item_a = add_labeled_item(&pane, "A", true, cx);
3870
3871        pane.update_in(cx, |pane, window, cx| {
3872            let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
3873            pane.pin_tab_at(ix, window, cx);
3874        });
3875        assert_item_labels(&pane, ["A*^!"], cx);
3876    }
3877
3878    #[gpui::test]
3879    async fn test_allow_pinning_non_dirty_item_at_max_tabs(cx: &mut TestAppContext) {
3880        init_test(cx);
3881        let fs = FakeFs::new(cx.executor());
3882
3883        let project = Project::test(fs, None, cx).await;
3884        let (workspace, cx) =
3885            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
3886        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
3887
3888        set_max_tabs(cx, Some(1));
3889        let item_a = add_labeled_item(&pane, "A", false, cx);
3890
3891        pane.update_in(cx, |pane, window, cx| {
3892            let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
3893            pane.pin_tab_at(ix, window, cx);
3894        });
3895        assert_item_labels(&pane, ["A*!"], cx);
3896    }
3897
3898    #[gpui::test]
3899    async fn test_pin_tabs_incrementally_at_max_capacity(cx: &mut TestAppContext) {
3900        init_test(cx);
3901        let fs = FakeFs::new(cx.executor());
3902
3903        let project = Project::test(fs, None, cx).await;
3904        let (workspace, cx) =
3905            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
3906        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
3907
3908        set_max_tabs(cx, Some(3));
3909
3910        let item_a = add_labeled_item(&pane, "A", false, cx);
3911        assert_item_labels(&pane, ["A*"], cx);
3912
3913        pane.update_in(cx, |pane, window, cx| {
3914            let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
3915            pane.pin_tab_at(ix, window, cx);
3916        });
3917        assert_item_labels(&pane, ["A*!"], cx);
3918
3919        let item_b = add_labeled_item(&pane, "B", false, cx);
3920        assert_item_labels(&pane, ["A!", "B*"], cx);
3921
3922        pane.update_in(cx, |pane, window, cx| {
3923            let ix = pane.index_for_item_id(item_b.item_id()).unwrap();
3924            pane.pin_tab_at(ix, window, cx);
3925        });
3926        assert_item_labels(&pane, ["A!", "B*!"], cx);
3927
3928        let item_c = add_labeled_item(&pane, "C", false, cx);
3929        assert_item_labels(&pane, ["A!", "B!", "C*"], cx);
3930
3931        pane.update_in(cx, |pane, window, cx| {
3932            let ix = pane.index_for_item_id(item_c.item_id()).unwrap();
3933            pane.pin_tab_at(ix, window, cx);
3934        });
3935        assert_item_labels(&pane, ["A!", "B!", "C*!"], cx);
3936    }
3937
3938    #[gpui::test]
3939    async fn test_pin_tabs_left_to_right_after_opening_at_max_capacity(cx: &mut TestAppContext) {
3940        init_test(cx);
3941        let fs = FakeFs::new(cx.executor());
3942
3943        let project = Project::test(fs, None, cx).await;
3944        let (workspace, cx) =
3945            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
3946        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
3947
3948        set_max_tabs(cx, Some(3));
3949
3950        let item_a = add_labeled_item(&pane, "A", false, cx);
3951        assert_item_labels(&pane, ["A*"], cx);
3952
3953        let item_b = add_labeled_item(&pane, "B", false, cx);
3954        assert_item_labels(&pane, ["A", "B*"], cx);
3955
3956        let item_c = add_labeled_item(&pane, "C", false, cx);
3957        assert_item_labels(&pane, ["A", "B", "C*"], cx);
3958
3959        pane.update_in(cx, |pane, window, cx| {
3960            let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
3961            pane.pin_tab_at(ix, window, cx);
3962        });
3963        assert_item_labels(&pane, ["A!", "B", "C*"], cx);
3964
3965        pane.update_in(cx, |pane, window, cx| {
3966            let ix = pane.index_for_item_id(item_b.item_id()).unwrap();
3967            pane.pin_tab_at(ix, window, cx);
3968        });
3969        assert_item_labels(&pane, ["A!", "B!", "C*"], cx);
3970
3971        pane.update_in(cx, |pane, window, cx| {
3972            let ix = pane.index_for_item_id(item_c.item_id()).unwrap();
3973            pane.pin_tab_at(ix, window, cx);
3974        });
3975        assert_item_labels(&pane, ["A!", "B!", "C*!"], cx);
3976    }
3977
3978    #[gpui::test]
3979    async fn test_pin_tabs_right_to_left_after_opening_at_max_capacity(cx: &mut TestAppContext) {
3980        init_test(cx);
3981        let fs = FakeFs::new(cx.executor());
3982
3983        let project = Project::test(fs, None, cx).await;
3984        let (workspace, cx) =
3985            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
3986        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
3987
3988        set_max_tabs(cx, Some(3));
3989
3990        let item_a = add_labeled_item(&pane, "A", false, cx);
3991        assert_item_labels(&pane, ["A*"], cx);
3992
3993        let item_b = add_labeled_item(&pane, "B", false, cx);
3994        assert_item_labels(&pane, ["A", "B*"], cx);
3995
3996        let item_c = add_labeled_item(&pane, "C", false, cx);
3997        assert_item_labels(&pane, ["A", "B", "C*"], cx);
3998
3999        pane.update_in(cx, |pane, window, cx| {
4000            let ix = pane.index_for_item_id(item_c.item_id()).unwrap();
4001            pane.pin_tab_at(ix, window, cx);
4002        });
4003        assert_item_labels(&pane, ["C*!", "A", "B"], cx);
4004
4005        pane.update_in(cx, |pane, window, cx| {
4006            let ix = pane.index_for_item_id(item_b.item_id()).unwrap();
4007            pane.pin_tab_at(ix, window, cx);
4008        });
4009        assert_item_labels(&pane, ["C!", "B*!", "A"], cx);
4010
4011        pane.update_in(cx, |pane, window, cx| {
4012            let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4013            pane.pin_tab_at(ix, window, cx);
4014        });
4015        assert_item_labels(&pane, ["C!", "B*!", "A!"], cx);
4016    }
4017
4018    #[gpui::test]
4019    async fn test_pinned_tabs_never_closed_at_max_tabs(cx: &mut TestAppContext) {
4020        init_test(cx);
4021        let fs = FakeFs::new(cx.executor());
4022
4023        let project = Project::test(fs, None, cx).await;
4024        let (workspace, cx) =
4025            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4026        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4027
4028        let item_a = add_labeled_item(&pane, "A", false, cx);
4029        pane.update_in(cx, |pane, window, cx| {
4030            let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4031            pane.pin_tab_at(ix, window, cx);
4032        });
4033
4034        let item_b = add_labeled_item(&pane, "B", false, cx);
4035        pane.update_in(cx, |pane, window, cx| {
4036            let ix = pane.index_for_item_id(item_b.item_id()).unwrap();
4037            pane.pin_tab_at(ix, window, cx);
4038        });
4039
4040        add_labeled_item(&pane, "C", false, cx);
4041        add_labeled_item(&pane, "D", false, cx);
4042        add_labeled_item(&pane, "E", false, cx);
4043        assert_item_labels(&pane, ["A!", "B!", "C", "D", "E*"], cx);
4044
4045        set_max_tabs(cx, Some(3));
4046        add_labeled_item(&pane, "F", false, cx);
4047        assert_item_labels(&pane, ["A!", "B!", "F*"], cx);
4048
4049        add_labeled_item(&pane, "G", false, cx);
4050        assert_item_labels(&pane, ["A!", "B!", "G*"], cx);
4051
4052        add_labeled_item(&pane, "H", false, cx);
4053        assert_item_labels(&pane, ["A!", "B!", "H*"], cx);
4054    }
4055
4056    #[gpui::test]
4057    async fn test_always_allows_one_unpinned_item_over_max_tabs_regardless_of_pinned_count(
4058        cx: &mut TestAppContext,
4059    ) {
4060        init_test(cx);
4061        let fs = FakeFs::new(cx.executor());
4062
4063        let project = Project::test(fs, None, cx).await;
4064        let (workspace, cx) =
4065            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4066        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4067
4068        set_max_tabs(cx, Some(3));
4069
4070        let item_a = add_labeled_item(&pane, "A", false, cx);
4071        pane.update_in(cx, |pane, window, cx| {
4072            let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4073            pane.pin_tab_at(ix, window, cx);
4074        });
4075
4076        let item_b = add_labeled_item(&pane, "B", false, cx);
4077        pane.update_in(cx, |pane, window, cx| {
4078            let ix = pane.index_for_item_id(item_b.item_id()).unwrap();
4079            pane.pin_tab_at(ix, window, cx);
4080        });
4081
4082        let item_c = add_labeled_item(&pane, "C", false, cx);
4083        pane.update_in(cx, |pane, window, cx| {
4084            let ix = pane.index_for_item_id(item_c.item_id()).unwrap();
4085            pane.pin_tab_at(ix, window, cx);
4086        });
4087
4088        assert_item_labels(&pane, ["A!", "B!", "C*!"], cx);
4089
4090        let item_d = add_labeled_item(&pane, "D", false, cx);
4091        assert_item_labels(&pane, ["A!", "B!", "C!", "D*"], cx);
4092
4093        pane.update_in(cx, |pane, window, cx| {
4094            let ix = pane.index_for_item_id(item_d.item_id()).unwrap();
4095            pane.pin_tab_at(ix, window, cx);
4096        });
4097        assert_item_labels(&pane, ["A!", "B!", "C!", "D*!"], cx);
4098
4099        add_labeled_item(&pane, "E", false, cx);
4100        assert_item_labels(&pane, ["A!", "B!", "C!", "D!", "E*"], cx);
4101
4102        add_labeled_item(&pane, "F", false, cx);
4103        assert_item_labels(&pane, ["A!", "B!", "C!", "D!", "F*"], cx);
4104    }
4105
4106    #[gpui::test]
4107    async fn test_can_open_one_item_when_all_tabs_are_dirty_at_max(cx: &mut TestAppContext) {
4108        init_test(cx);
4109        let fs = FakeFs::new(cx.executor());
4110
4111        let project = Project::test(fs, None, cx).await;
4112        let (workspace, cx) =
4113            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4114        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4115
4116        set_max_tabs(cx, Some(3));
4117
4118        add_labeled_item(&pane, "A", true, cx);
4119        assert_item_labels(&pane, ["A*^"], cx);
4120
4121        add_labeled_item(&pane, "B", true, cx);
4122        assert_item_labels(&pane, ["A^", "B*^"], cx);
4123
4124        add_labeled_item(&pane, "C", true, cx);
4125        assert_item_labels(&pane, ["A^", "B^", "C*^"], cx);
4126
4127        add_labeled_item(&pane, "D", false, cx);
4128        assert_item_labels(&pane, ["A^", "B^", "C^", "D*"], cx);
4129
4130        add_labeled_item(&pane, "E", false, cx);
4131        assert_item_labels(&pane, ["A^", "B^", "C^", "E*"], cx);
4132
4133        add_labeled_item(&pane, "F", false, cx);
4134        assert_item_labels(&pane, ["A^", "B^", "C^", "F*"], cx);
4135
4136        add_labeled_item(&pane, "G", true, cx);
4137        assert_item_labels(&pane, ["A^", "B^", "C^", "G*^"], cx);
4138    }
4139
4140    #[gpui::test]
4141    async fn test_toggle_pin_tab(cx: &mut TestAppContext) {
4142        init_test(cx);
4143        let fs = FakeFs::new(cx.executor());
4144
4145        let project = Project::test(fs, None, cx).await;
4146        let (workspace, cx) =
4147            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4148        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4149
4150        set_labeled_items(&pane, ["A", "B*", "C"], cx);
4151        assert_item_labels(&pane, ["A", "B*", "C"], cx);
4152
4153        pane.update_in(cx, |pane, window, cx| {
4154            pane.toggle_pin_tab(&TogglePinTab, window, cx);
4155        });
4156        assert_item_labels(&pane, ["B*!", "A", "C"], cx);
4157
4158        pane.update_in(cx, |pane, window, cx| {
4159            pane.toggle_pin_tab(&TogglePinTab, window, cx);
4160        });
4161        assert_item_labels(&pane, ["B*", "A", "C"], cx);
4162    }
4163
4164    #[gpui::test]
4165    async fn test_add_item_with_new_item(cx: &mut TestAppContext) {
4166        init_test(cx);
4167        let fs = FakeFs::new(cx.executor());
4168
4169        let project = Project::test(fs, None, cx).await;
4170        let (workspace, cx) =
4171            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4172        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4173
4174        // 1. Add with a destination index
4175        //   a. Add before the active item
4176        set_labeled_items(&pane, ["A", "B*", "C"], cx);
4177        pane.update_in(cx, |pane, window, cx| {
4178            pane.add_item(
4179                Box::new(cx.new(|cx| TestItem::new(cx).with_label("D"))),
4180                false,
4181                false,
4182                Some(0),
4183                window,
4184                cx,
4185            );
4186        });
4187        assert_item_labels(&pane, ["D*", "A", "B", "C"], cx);
4188
4189        //   b. Add after the active item
4190        set_labeled_items(&pane, ["A", "B*", "C"], cx);
4191        pane.update_in(cx, |pane, window, cx| {
4192            pane.add_item(
4193                Box::new(cx.new(|cx| TestItem::new(cx).with_label("D"))),
4194                false,
4195                false,
4196                Some(2),
4197                window,
4198                cx,
4199            );
4200        });
4201        assert_item_labels(&pane, ["A", "B", "D*", "C"], cx);
4202
4203        //   c. Add at the end of the item list (including off the length)
4204        set_labeled_items(&pane, ["A", "B*", "C"], cx);
4205        pane.update_in(cx, |pane, window, cx| {
4206            pane.add_item(
4207                Box::new(cx.new(|cx| TestItem::new(cx).with_label("D"))),
4208                false,
4209                false,
4210                Some(5),
4211                window,
4212                cx,
4213            );
4214        });
4215        assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
4216
4217        // 2. Add without a destination index
4218        //   a. Add with active item at the start of the item list
4219        set_labeled_items(&pane, ["A*", "B", "C"], cx);
4220        pane.update_in(cx, |pane, window, cx| {
4221            pane.add_item(
4222                Box::new(cx.new(|cx| TestItem::new(cx).with_label("D"))),
4223                false,
4224                false,
4225                None,
4226                window,
4227                cx,
4228            );
4229        });
4230        set_labeled_items(&pane, ["A", "D*", "B", "C"], cx);
4231
4232        //   b. Add with active item at the end of the item list
4233        set_labeled_items(&pane, ["A", "B", "C*"], cx);
4234        pane.update_in(cx, |pane, window, cx| {
4235            pane.add_item(
4236                Box::new(cx.new(|cx| TestItem::new(cx).with_label("D"))),
4237                false,
4238                false,
4239                None,
4240                window,
4241                cx,
4242            );
4243        });
4244        assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
4245    }
4246
4247    #[gpui::test]
4248    async fn test_add_item_with_existing_item(cx: &mut TestAppContext) {
4249        init_test(cx);
4250        let fs = FakeFs::new(cx.executor());
4251
4252        let project = Project::test(fs, None, cx).await;
4253        let (workspace, cx) =
4254            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4255        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4256
4257        // 1. Add with a destination index
4258        //   1a. Add before the active item
4259        let [_, _, _, d] = set_labeled_items(&pane, ["A", "B*", "C", "D"], cx);
4260        pane.update_in(cx, |pane, window, cx| {
4261            pane.add_item(d, false, false, Some(0), window, cx);
4262        });
4263        assert_item_labels(&pane, ["D*", "A", "B", "C"], cx);
4264
4265        //   1b. Add after the active item
4266        let [_, _, _, d] = set_labeled_items(&pane, ["A", "B*", "C", "D"], cx);
4267        pane.update_in(cx, |pane, window, cx| {
4268            pane.add_item(d, false, false, Some(2), window, cx);
4269        });
4270        assert_item_labels(&pane, ["A", "B", "D*", "C"], cx);
4271
4272        //   1c. Add at the end of the item list (including off the length)
4273        let [a, _, _, _] = set_labeled_items(&pane, ["A", "B*", "C", "D"], cx);
4274        pane.update_in(cx, |pane, window, cx| {
4275            pane.add_item(a, false, false, Some(5), window, cx);
4276        });
4277        assert_item_labels(&pane, ["B", "C", "D", "A*"], cx);
4278
4279        //   1d. Add same item to active index
4280        let [_, b, _] = set_labeled_items(&pane, ["A", "B*", "C"], cx);
4281        pane.update_in(cx, |pane, window, cx| {
4282            pane.add_item(b, false, false, Some(1), window, cx);
4283        });
4284        assert_item_labels(&pane, ["A", "B*", "C"], cx);
4285
4286        //   1e. Add item to index after same item in last position
4287        let [_, _, c] = set_labeled_items(&pane, ["A", "B*", "C"], cx);
4288        pane.update_in(cx, |pane, window, cx| {
4289            pane.add_item(c, false, false, Some(2), window, cx);
4290        });
4291        assert_item_labels(&pane, ["A", "B", "C*"], cx);
4292
4293        // 2. Add without a destination index
4294        //   2a. Add with active item at the start of the item list
4295        let [_, _, _, d] = set_labeled_items(&pane, ["A*", "B", "C", "D"], cx);
4296        pane.update_in(cx, |pane, window, cx| {
4297            pane.add_item(d, false, false, None, window, cx);
4298        });
4299        assert_item_labels(&pane, ["A", "D*", "B", "C"], cx);
4300
4301        //   2b. Add with active item at the end of the item list
4302        let [a, _, _, _] = set_labeled_items(&pane, ["A", "B", "C", "D*"], cx);
4303        pane.update_in(cx, |pane, window, cx| {
4304            pane.add_item(a, false, false, None, window, cx);
4305        });
4306        assert_item_labels(&pane, ["B", "C", "D", "A*"], cx);
4307
4308        //   2c. Add active item to active item at end of list
4309        let [_, _, c] = set_labeled_items(&pane, ["A", "B", "C*"], cx);
4310        pane.update_in(cx, |pane, window, cx| {
4311            pane.add_item(c, false, false, None, window, cx);
4312        });
4313        assert_item_labels(&pane, ["A", "B", "C*"], cx);
4314
4315        //   2d. Add active item to active item at start of list
4316        let [a, _, _] = set_labeled_items(&pane, ["A*", "B", "C"], cx);
4317        pane.update_in(cx, |pane, window, cx| {
4318            pane.add_item(a, false, false, None, window, cx);
4319        });
4320        assert_item_labels(&pane, ["A*", "B", "C"], cx);
4321    }
4322
4323    #[gpui::test]
4324    async fn test_add_item_with_same_project_entries(cx: &mut TestAppContext) {
4325        init_test(cx);
4326        let fs = FakeFs::new(cx.executor());
4327
4328        let project = Project::test(fs, None, cx).await;
4329        let (workspace, cx) =
4330            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4331        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4332
4333        // singleton view
4334        pane.update_in(cx, |pane, window, cx| {
4335            pane.add_item(
4336                Box::new(cx.new(|cx| {
4337                    TestItem::new(cx)
4338                        .with_singleton(true)
4339                        .with_label("buffer 1")
4340                        .with_project_items(&[TestProjectItem::new(1, "one.txt", cx)])
4341                })),
4342                false,
4343                false,
4344                None,
4345                window,
4346                cx,
4347            );
4348        });
4349        assert_item_labels(&pane, ["buffer 1*"], cx);
4350
4351        // new singleton view with the same project entry
4352        pane.update_in(cx, |pane, window, cx| {
4353            pane.add_item(
4354                Box::new(cx.new(|cx| {
4355                    TestItem::new(cx)
4356                        .with_singleton(true)
4357                        .with_label("buffer 1")
4358                        .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
4359                })),
4360                false,
4361                false,
4362                None,
4363                window,
4364                cx,
4365            );
4366        });
4367        assert_item_labels(&pane, ["buffer 1*"], cx);
4368
4369        // new singleton view with different project entry
4370        pane.update_in(cx, |pane, window, cx| {
4371            pane.add_item(
4372                Box::new(cx.new(|cx| {
4373                    TestItem::new(cx)
4374                        .with_singleton(true)
4375                        .with_label("buffer 2")
4376                        .with_project_items(&[TestProjectItem::new(2, "2.txt", cx)])
4377                })),
4378                false,
4379                false,
4380                None,
4381                window,
4382                cx,
4383            );
4384        });
4385        assert_item_labels(&pane, ["buffer 1", "buffer 2*"], cx);
4386
4387        // new multibuffer view with the same project entry
4388        pane.update_in(cx, |pane, window, cx| {
4389            pane.add_item(
4390                Box::new(cx.new(|cx| {
4391                    TestItem::new(cx)
4392                        .with_singleton(false)
4393                        .with_label("multibuffer 1")
4394                        .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
4395                })),
4396                false,
4397                false,
4398                None,
4399                window,
4400                cx,
4401            );
4402        });
4403        assert_item_labels(&pane, ["buffer 1", "buffer 2", "multibuffer 1*"], cx);
4404
4405        // another multibuffer view with the same project entry
4406        pane.update_in(cx, |pane, window, cx| {
4407            pane.add_item(
4408                Box::new(cx.new(|cx| {
4409                    TestItem::new(cx)
4410                        .with_singleton(false)
4411                        .with_label("multibuffer 1b")
4412                        .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
4413                })),
4414                false,
4415                false,
4416                None,
4417                window,
4418                cx,
4419            );
4420        });
4421        assert_item_labels(
4422            &pane,
4423            ["buffer 1", "buffer 2", "multibuffer 1", "multibuffer 1b*"],
4424            cx,
4425        );
4426    }
4427
4428    #[gpui::test]
4429    async fn test_remove_item_ordering_history(cx: &mut TestAppContext) {
4430        init_test(cx);
4431        let fs = FakeFs::new(cx.executor());
4432
4433        let project = Project::test(fs, None, cx).await;
4434        let (workspace, cx) =
4435            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4436        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4437
4438        add_labeled_item(&pane, "A", false, cx);
4439        add_labeled_item(&pane, "B", false, cx);
4440        add_labeled_item(&pane, "C", false, cx);
4441        add_labeled_item(&pane, "D", false, cx);
4442        assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
4443
4444        pane.update_in(cx, |pane, window, cx| {
4445            pane.activate_item(1, false, false, window, cx)
4446        });
4447        add_labeled_item(&pane, "1", false, cx);
4448        assert_item_labels(&pane, ["A", "B", "1*", "C", "D"], cx);
4449
4450        pane.update_in(cx, |pane, window, cx| {
4451            pane.close_active_item(
4452                &CloseActiveItem {
4453                    save_intent: None,
4454                    close_pinned: false,
4455                },
4456                window,
4457                cx,
4458            )
4459        })
4460        .await
4461        .unwrap();
4462        assert_item_labels(&pane, ["A", "B*", "C", "D"], cx);
4463
4464        pane.update_in(cx, |pane, window, cx| {
4465            pane.activate_item(3, false, false, window, cx)
4466        });
4467        assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
4468
4469        pane.update_in(cx, |pane, window, cx| {
4470            pane.close_active_item(
4471                &CloseActiveItem {
4472                    save_intent: None,
4473                    close_pinned: false,
4474                },
4475                window,
4476                cx,
4477            )
4478        })
4479        .await
4480        .unwrap();
4481        assert_item_labels(&pane, ["A", "B*", "C"], cx);
4482
4483        pane.update_in(cx, |pane, window, cx| {
4484            pane.close_active_item(
4485                &CloseActiveItem {
4486                    save_intent: None,
4487                    close_pinned: false,
4488                },
4489                window,
4490                cx,
4491            )
4492        })
4493        .await
4494        .unwrap();
4495        assert_item_labels(&pane, ["A", "C*"], cx);
4496
4497        pane.update_in(cx, |pane, window, cx| {
4498            pane.close_active_item(
4499                &CloseActiveItem {
4500                    save_intent: None,
4501                    close_pinned: false,
4502                },
4503                window,
4504                cx,
4505            )
4506        })
4507        .await
4508        .unwrap();
4509        assert_item_labels(&pane, ["A*"], cx);
4510    }
4511
4512    #[gpui::test]
4513    async fn test_remove_item_ordering_neighbour(cx: &mut TestAppContext) {
4514        init_test(cx);
4515        cx.update_global::<SettingsStore, ()>(|s, cx| {
4516            s.update_user_settings::<ItemSettings>(cx, |s| {
4517                s.activate_on_close = Some(ActivateOnClose::Neighbour);
4518            });
4519        });
4520        let fs = FakeFs::new(cx.executor());
4521
4522        let project = Project::test(fs, None, cx).await;
4523        let (workspace, cx) =
4524            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4525        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4526
4527        add_labeled_item(&pane, "A", false, cx);
4528        add_labeled_item(&pane, "B", false, cx);
4529        add_labeled_item(&pane, "C", false, cx);
4530        add_labeled_item(&pane, "D", false, cx);
4531        assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
4532
4533        pane.update_in(cx, |pane, window, cx| {
4534            pane.activate_item(1, false, false, window, cx)
4535        });
4536        add_labeled_item(&pane, "1", false, cx);
4537        assert_item_labels(&pane, ["A", "B", "1*", "C", "D"], cx);
4538
4539        pane.update_in(cx, |pane, window, cx| {
4540            pane.close_active_item(
4541                &CloseActiveItem {
4542                    save_intent: None,
4543                    close_pinned: false,
4544                },
4545                window,
4546                cx,
4547            )
4548        })
4549        .await
4550        .unwrap();
4551        assert_item_labels(&pane, ["A", "B", "C*", "D"], cx);
4552
4553        pane.update_in(cx, |pane, window, cx| {
4554            pane.activate_item(3, false, false, window, cx)
4555        });
4556        assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
4557
4558        pane.update_in(cx, |pane, window, cx| {
4559            pane.close_active_item(
4560                &CloseActiveItem {
4561                    save_intent: None,
4562                    close_pinned: false,
4563                },
4564                window,
4565                cx,
4566            )
4567        })
4568        .await
4569        .unwrap();
4570        assert_item_labels(&pane, ["A", "B", "C*"], cx);
4571
4572        pane.update_in(cx, |pane, window, cx| {
4573            pane.close_active_item(
4574                &CloseActiveItem {
4575                    save_intent: None,
4576                    close_pinned: false,
4577                },
4578                window,
4579                cx,
4580            )
4581        })
4582        .await
4583        .unwrap();
4584        assert_item_labels(&pane, ["A", "B*"], cx);
4585
4586        pane.update_in(cx, |pane, window, cx| {
4587            pane.close_active_item(
4588                &CloseActiveItem {
4589                    save_intent: None,
4590                    close_pinned: false,
4591                },
4592                window,
4593                cx,
4594            )
4595        })
4596        .await
4597        .unwrap();
4598        assert_item_labels(&pane, ["A*"], cx);
4599    }
4600
4601    #[gpui::test]
4602    async fn test_remove_item_ordering_left_neighbour(cx: &mut TestAppContext) {
4603        init_test(cx);
4604        cx.update_global::<SettingsStore, ()>(|s, cx| {
4605            s.update_user_settings::<ItemSettings>(cx, |s| {
4606                s.activate_on_close = Some(ActivateOnClose::LeftNeighbour);
4607            });
4608        });
4609        let fs = FakeFs::new(cx.executor());
4610
4611        let project = Project::test(fs, None, cx).await;
4612        let (workspace, cx) =
4613            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4614        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4615
4616        add_labeled_item(&pane, "A", false, cx);
4617        add_labeled_item(&pane, "B", false, cx);
4618        add_labeled_item(&pane, "C", false, cx);
4619        add_labeled_item(&pane, "D", false, cx);
4620        assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
4621
4622        pane.update_in(cx, |pane, window, cx| {
4623            pane.activate_item(1, false, false, window, cx)
4624        });
4625        add_labeled_item(&pane, "1", false, cx);
4626        assert_item_labels(&pane, ["A", "B", "1*", "C", "D"], cx);
4627
4628        pane.update_in(cx, |pane, window, cx| {
4629            pane.close_active_item(
4630                &CloseActiveItem {
4631                    save_intent: None,
4632                    close_pinned: false,
4633                },
4634                window,
4635                cx,
4636            )
4637        })
4638        .await
4639        .unwrap();
4640        assert_item_labels(&pane, ["A", "B*", "C", "D"], cx);
4641
4642        pane.update_in(cx, |pane, window, cx| {
4643            pane.activate_item(3, false, false, window, cx)
4644        });
4645        assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
4646
4647        pane.update_in(cx, |pane, window, cx| {
4648            pane.close_active_item(
4649                &CloseActiveItem {
4650                    save_intent: None,
4651                    close_pinned: false,
4652                },
4653                window,
4654                cx,
4655            )
4656        })
4657        .await
4658        .unwrap();
4659        assert_item_labels(&pane, ["A", "B", "C*"], cx);
4660
4661        pane.update_in(cx, |pane, window, cx| {
4662            pane.activate_item(0, false, false, window, cx)
4663        });
4664        assert_item_labels(&pane, ["A*", "B", "C"], cx);
4665
4666        pane.update_in(cx, |pane, window, cx| {
4667            pane.close_active_item(
4668                &CloseActiveItem {
4669                    save_intent: None,
4670                    close_pinned: false,
4671                },
4672                window,
4673                cx,
4674            )
4675        })
4676        .await
4677        .unwrap();
4678        assert_item_labels(&pane, ["B*", "C"], cx);
4679
4680        pane.update_in(cx, |pane, window, cx| {
4681            pane.close_active_item(
4682                &CloseActiveItem {
4683                    save_intent: None,
4684                    close_pinned: false,
4685                },
4686                window,
4687                cx,
4688            )
4689        })
4690        .await
4691        .unwrap();
4692        assert_item_labels(&pane, ["C*"], cx);
4693    }
4694
4695    #[gpui::test]
4696    async fn test_close_inactive_items(cx: &mut TestAppContext) {
4697        init_test(cx);
4698        let fs = FakeFs::new(cx.executor());
4699
4700        let project = Project::test(fs, None, cx).await;
4701        let (workspace, cx) =
4702            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4703        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4704
4705        let item_a = add_labeled_item(&pane, "A", false, cx);
4706        pane.update_in(cx, |pane, window, cx| {
4707            let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4708            pane.pin_tab_at(ix, window, cx);
4709        });
4710        assert_item_labels(&pane, ["A*!"], cx);
4711
4712        let item_b = add_labeled_item(&pane, "B", false, cx);
4713        pane.update_in(cx, |pane, window, cx| {
4714            let ix = pane.index_for_item_id(item_b.item_id()).unwrap();
4715            pane.pin_tab_at(ix, window, cx);
4716        });
4717        assert_item_labels(&pane, ["A!", "B*!"], cx);
4718
4719        add_labeled_item(&pane, "C", false, cx);
4720        assert_item_labels(&pane, ["A!", "B!", "C*"], cx);
4721
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_inactive_items(
4728                &CloseInactiveItems {
4729                    save_intent: None,
4730                    close_pinned: false,
4731                },
4732                window,
4733                cx,
4734            )
4735        })
4736        .await
4737        .unwrap();
4738        assert_item_labels(&pane, ["A!", "B!", "E*"], cx);
4739    }
4740
4741    #[gpui::test]
4742    async fn test_close_clean_items(cx: &mut TestAppContext) {
4743        init_test(cx);
4744        let fs = FakeFs::new(cx.executor());
4745
4746        let project = Project::test(fs, None, cx).await;
4747        let (workspace, cx) =
4748            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4749        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4750
4751        add_labeled_item(&pane, "A", true, cx);
4752        add_labeled_item(&pane, "B", false, cx);
4753        add_labeled_item(&pane, "C", true, cx);
4754        add_labeled_item(&pane, "D", false, cx);
4755        add_labeled_item(&pane, "E", false, cx);
4756        assert_item_labels(&pane, ["A^", "B", "C^", "D", "E*"], cx);
4757
4758        pane.update_in(cx, |pane, window, cx| {
4759            pane.close_clean_items(
4760                &CloseCleanItems {
4761                    close_pinned: false,
4762                },
4763                window,
4764                cx,
4765            )
4766        })
4767        .await
4768        .unwrap();
4769        assert_item_labels(&pane, ["A^", "C*^"], cx);
4770    }
4771
4772    #[gpui::test]
4773    async fn test_close_items_to_the_left(cx: &mut TestAppContext) {
4774        init_test(cx);
4775        let fs = FakeFs::new(cx.executor());
4776
4777        let project = Project::test(fs, None, cx).await;
4778        let (workspace, cx) =
4779            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4780        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4781
4782        set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx);
4783
4784        pane.update_in(cx, |pane, window, cx| {
4785            pane.close_items_to_the_left_by_id(
4786                None,
4787                &CloseItemsToTheLeft {
4788                    close_pinned: false,
4789                },
4790                window,
4791                cx,
4792            )
4793        })
4794        .await
4795        .unwrap();
4796        assert_item_labels(&pane, ["C*", "D", "E"], cx);
4797    }
4798
4799    #[gpui::test]
4800    async fn test_close_items_to_the_right(cx: &mut TestAppContext) {
4801        init_test(cx);
4802        let fs = FakeFs::new(cx.executor());
4803
4804        let project = Project::test(fs, None, cx).await;
4805        let (workspace, cx) =
4806            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4807        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4808
4809        set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx);
4810
4811        pane.update_in(cx, |pane, window, cx| {
4812            pane.close_items_to_the_right_by_id(
4813                None,
4814                &CloseItemsToTheRight {
4815                    close_pinned: false,
4816                },
4817                window,
4818                cx,
4819            )
4820        })
4821        .await
4822        .unwrap();
4823        assert_item_labels(&pane, ["A", "B", "C*"], cx);
4824    }
4825
4826    #[gpui::test]
4827    async fn test_close_all_items(cx: &mut TestAppContext) {
4828        init_test(cx);
4829        let fs = FakeFs::new(cx.executor());
4830
4831        let project = Project::test(fs, None, cx).await;
4832        let (workspace, cx) =
4833            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4834        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4835
4836        let item_a = add_labeled_item(&pane, "A", false, cx);
4837        add_labeled_item(&pane, "B", false, cx);
4838        add_labeled_item(&pane, "C", false, cx);
4839        assert_item_labels(&pane, ["A", "B", "C*"], cx);
4840
4841        pane.update_in(cx, |pane, window, cx| {
4842            let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4843            pane.pin_tab_at(ix, window, cx);
4844            pane.close_all_items(
4845                &CloseAllItems {
4846                    save_intent: None,
4847                    close_pinned: false,
4848                },
4849                window,
4850                cx,
4851            )
4852        })
4853        .await
4854        .unwrap();
4855        assert_item_labels(&pane, ["A*!"], cx);
4856
4857        pane.update_in(cx, |pane, window, cx| {
4858            let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4859            pane.unpin_tab_at(ix, window, cx);
4860            pane.close_all_items(
4861                &CloseAllItems {
4862                    save_intent: None,
4863                    close_pinned: false,
4864                },
4865                window,
4866                cx,
4867            )
4868        })
4869        .await
4870        .unwrap();
4871
4872        assert_item_labels(&pane, [], cx);
4873
4874        add_labeled_item(&pane, "A", true, cx).update(cx, |item, cx| {
4875            item.project_items
4876                .push(TestProjectItem::new_dirty(1, "A.txt", cx))
4877        });
4878        add_labeled_item(&pane, "B", true, cx).update(cx, |item, cx| {
4879            item.project_items
4880                .push(TestProjectItem::new_dirty(2, "B.txt", cx))
4881        });
4882        add_labeled_item(&pane, "C", true, cx).update(cx, |item, cx| {
4883            item.project_items
4884                .push(TestProjectItem::new_dirty(3, "C.txt", cx))
4885        });
4886        assert_item_labels(&pane, ["A^", "B^", "C*^"], cx);
4887
4888        let save = pane.update_in(cx, |pane, window, cx| {
4889            pane.close_all_items(
4890                &CloseAllItems {
4891                    save_intent: None,
4892                    close_pinned: false,
4893                },
4894                window,
4895                cx,
4896            )
4897        });
4898
4899        cx.executor().run_until_parked();
4900        cx.simulate_prompt_answer("Save all");
4901        save.await.unwrap();
4902        assert_item_labels(&pane, [], cx);
4903
4904        add_labeled_item(&pane, "A", true, cx);
4905        add_labeled_item(&pane, "B", true, cx);
4906        add_labeled_item(&pane, "C", true, cx);
4907        assert_item_labels(&pane, ["A^", "B^", "C*^"], cx);
4908        let save = pane.update_in(cx, |pane, window, cx| {
4909            pane.close_all_items(
4910                &CloseAllItems {
4911                    save_intent: None,
4912                    close_pinned: false,
4913                },
4914                window,
4915                cx,
4916            )
4917        });
4918
4919        cx.executor().run_until_parked();
4920        cx.simulate_prompt_answer("Discard all");
4921        save.await.unwrap();
4922        assert_item_labels(&pane, [], cx);
4923    }
4924
4925    #[gpui::test]
4926    async fn test_close_with_save_intent(cx: &mut TestAppContext) {
4927        init_test(cx);
4928        let fs = FakeFs::new(cx.executor());
4929
4930        let project = Project::test(fs, None, cx).await;
4931        let (workspace, cx) =
4932            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
4933        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4934
4935        let a = cx.update(|_, cx| TestProjectItem::new_dirty(1, "A.txt", cx));
4936        let b = cx.update(|_, cx| TestProjectItem::new_dirty(1, "B.txt", cx));
4937        let c = cx.update(|_, cx| TestProjectItem::new_dirty(1, "C.txt", cx));
4938
4939        add_labeled_item(&pane, "AB", true, cx).update(cx, |item, _| {
4940            item.project_items.push(a.clone());
4941            item.project_items.push(b.clone());
4942        });
4943        add_labeled_item(&pane, "C", true, cx)
4944            .update(cx, |item, _| item.project_items.push(c.clone()));
4945        assert_item_labels(&pane, ["AB^", "C*^"], cx);
4946
4947        pane.update_in(cx, |pane, window, cx| {
4948            pane.close_all_items(
4949                &CloseAllItems {
4950                    save_intent: Some(SaveIntent::Save),
4951                    close_pinned: false,
4952                },
4953                window,
4954                cx,
4955            )
4956        })
4957        .await
4958        .unwrap();
4959
4960        assert_item_labels(&pane, [], cx);
4961        cx.update(|_, cx| {
4962            assert!(!a.read(cx).is_dirty);
4963            assert!(!b.read(cx).is_dirty);
4964            assert!(!c.read(cx).is_dirty);
4965        });
4966    }
4967
4968    #[gpui::test]
4969    async fn test_close_all_items_including_pinned(cx: &mut TestAppContext) {
4970        init_test(cx);
4971        let fs = FakeFs::new(cx.executor());
4972
4973        let project = Project::test(fs, None, cx).await;
4974        let (workspace, cx) =
4975            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
4976        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4977
4978        let item_a = add_labeled_item(&pane, "A", false, cx);
4979        add_labeled_item(&pane, "B", false, cx);
4980        add_labeled_item(&pane, "C", false, cx);
4981        assert_item_labels(&pane, ["A", "B", "C*"], cx);
4982
4983        pane.update_in(cx, |pane, window, cx| {
4984            let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4985            pane.pin_tab_at(ix, window, cx);
4986            pane.close_all_items(
4987                &CloseAllItems {
4988                    save_intent: None,
4989                    close_pinned: true,
4990                },
4991                window,
4992                cx,
4993            )
4994        })
4995        .await
4996        .unwrap();
4997        assert_item_labels(&pane, [], cx);
4998    }
4999
5000    #[gpui::test]
5001    async fn test_close_pinned_tab_with_non_pinned_in_same_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        // Non-pinned tabs in same pane
5009        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
5010        add_labeled_item(&pane, "A", false, cx);
5011        add_labeled_item(&pane, "B", false, cx);
5012        add_labeled_item(&pane, "C", false, cx);
5013        pane.update_in(cx, |pane, window, cx| {
5014            pane.pin_tab_at(0, window, cx);
5015        });
5016        set_labeled_items(&pane, ["A*", "B", "C"], cx);
5017        pane.update_in(cx, |pane, window, cx| {
5018            pane.close_active_item(
5019                &CloseActiveItem {
5020                    save_intent: None,
5021                    close_pinned: false,
5022                },
5023                window,
5024                cx,
5025            )
5026            .unwrap();
5027        });
5028        // Non-pinned tab should be active
5029        assert_item_labels(&pane, ["A!", "B*", "C"], cx);
5030    }
5031
5032    #[gpui::test]
5033    async fn test_close_pinned_tab_with_non_pinned_in_different_pane(cx: &mut TestAppContext) {
5034        init_test(cx);
5035        let fs = FakeFs::new(cx.executor());
5036        let project = Project::test(fs, None, cx).await;
5037        let (workspace, cx) =
5038            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
5039
5040        // No non-pinned tabs in same pane, non-pinned tabs in another pane
5041        let pane1 = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
5042        let pane2 = workspace.update_in(cx, |workspace, window, cx| {
5043            workspace.split_pane(pane1.clone(), SplitDirection::Right, window, cx)
5044        });
5045        add_labeled_item(&pane1, "A", false, cx);
5046        pane1.update_in(cx, |pane, window, cx| {
5047            pane.pin_tab_at(0, window, cx);
5048        });
5049        set_labeled_items(&pane1, ["A*"], cx);
5050        add_labeled_item(&pane2, "B", false, cx);
5051        set_labeled_items(&pane2, ["B"], cx);
5052        pane1.update_in(cx, |pane, window, cx| {
5053            pane.close_active_item(
5054                &CloseActiveItem {
5055                    save_intent: None,
5056                    close_pinned: false,
5057                },
5058                window,
5059                cx,
5060            )
5061            .unwrap();
5062        });
5063        //  Non-pinned tab of other pane should be active
5064        assert_item_labels(&pane2, ["B*"], cx);
5065    }
5066
5067    #[gpui::test]
5068    async fn ensure_item_closing_actions_do_not_panic_when_no_items_exist(cx: &mut TestAppContext) {
5069        init_test(cx);
5070        let fs = FakeFs::new(cx.executor());
5071        let project = Project::test(fs, None, cx).await;
5072        let (workspace, cx) =
5073            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
5074
5075        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
5076        assert_item_labels(&pane, [], cx);
5077
5078        pane.update_in(cx, |pane, window, cx| {
5079            pane.close_active_item(
5080                &CloseActiveItem {
5081                    save_intent: None,
5082                    close_pinned: false,
5083                },
5084                window,
5085                cx,
5086            )
5087        })
5088        .await
5089        .unwrap();
5090
5091        pane.update_in(cx, |pane, window, cx| {
5092            pane.close_inactive_items(
5093                &CloseInactiveItems {
5094                    save_intent: None,
5095                    close_pinned: false,
5096                },
5097                window,
5098                cx,
5099            )
5100        })
5101        .await
5102        .unwrap();
5103
5104        pane.update_in(cx, |pane, window, cx| {
5105            pane.close_all_items(
5106                &CloseAllItems {
5107                    save_intent: None,
5108                    close_pinned: false,
5109                },
5110                window,
5111                cx,
5112            )
5113        })
5114        .await
5115        .unwrap();
5116
5117        pane.update_in(cx, |pane, window, cx| {
5118            pane.close_clean_items(
5119                &CloseCleanItems {
5120                    close_pinned: false,
5121                },
5122                window,
5123                cx,
5124            )
5125        })
5126        .await
5127        .unwrap();
5128
5129        pane.update_in(cx, |pane, window, cx| {
5130            pane.close_items_to_the_right_by_id(
5131                None,
5132                &CloseItemsToTheRight {
5133                    close_pinned: false,
5134                },
5135                window,
5136                cx,
5137            )
5138        })
5139        .await
5140        .unwrap();
5141
5142        pane.update_in(cx, |pane, window, cx| {
5143            pane.close_items_to_the_left_by_id(
5144                None,
5145                &CloseItemsToTheLeft {
5146                    close_pinned: false,
5147                },
5148                window,
5149                cx,
5150            )
5151        })
5152        .await
5153        .unwrap();
5154    }
5155
5156    fn init_test(cx: &mut TestAppContext) {
5157        cx.update(|cx| {
5158            let settings_store = SettingsStore::test(cx);
5159            cx.set_global(settings_store);
5160            theme::init(LoadThemes::JustBase, cx);
5161            crate::init_settings(cx);
5162            Project::init_settings(cx);
5163        });
5164    }
5165
5166    fn set_max_tabs(cx: &mut TestAppContext, value: Option<usize>) {
5167        cx.update_global(|store: &mut SettingsStore, cx| {
5168            store.update_user_settings::<WorkspaceSettings>(cx, |settings| {
5169                settings.max_tabs = value.map(|v| NonZero::new(v).unwrap())
5170            });
5171        });
5172    }
5173
5174    fn add_labeled_item(
5175        pane: &Entity<Pane>,
5176        label: &str,
5177        is_dirty: bool,
5178        cx: &mut VisualTestContext,
5179    ) -> Box<Entity<TestItem>> {
5180        pane.update_in(cx, |pane, window, cx| {
5181            let labeled_item =
5182                Box::new(cx.new(|cx| TestItem::new(cx).with_label(label).with_dirty(is_dirty)));
5183            pane.add_item(labeled_item.clone(), false, false, None, window, cx);
5184            labeled_item
5185        })
5186    }
5187
5188    fn set_labeled_items<const COUNT: usize>(
5189        pane: &Entity<Pane>,
5190        labels: [&str; COUNT],
5191        cx: &mut VisualTestContext,
5192    ) -> [Box<Entity<TestItem>>; COUNT] {
5193        pane.update_in(cx, |pane, window, cx| {
5194            pane.items.clear();
5195            let mut active_item_index = 0;
5196
5197            let mut index = 0;
5198            let items = labels.map(|mut label| {
5199                if label.ends_with('*') {
5200                    label = label.trim_end_matches('*');
5201                    active_item_index = index;
5202                }
5203
5204                let labeled_item = Box::new(cx.new(|cx| TestItem::new(cx).with_label(label)));
5205                pane.add_item(labeled_item.clone(), false, false, None, window, cx);
5206                index += 1;
5207                labeled_item
5208            });
5209
5210            pane.activate_item(active_item_index, false, false, window, cx);
5211
5212            items
5213        })
5214    }
5215
5216    // Assert the item label, with the active item label suffixed with a '*'
5217    #[track_caller]
5218    fn assert_item_labels<const COUNT: usize>(
5219        pane: &Entity<Pane>,
5220        expected_states: [&str; COUNT],
5221        cx: &mut VisualTestContext,
5222    ) {
5223        let actual_states = pane.update(cx, |pane, cx| {
5224            pane.items
5225                .iter()
5226                .enumerate()
5227                .map(|(ix, item)| {
5228                    let mut state = item
5229                        .to_any()
5230                        .downcast::<TestItem>()
5231                        .unwrap()
5232                        .read(cx)
5233                        .label
5234                        .clone();
5235                    if ix == pane.active_item_index {
5236                        state.push('*');
5237                    }
5238                    if item.is_dirty(cx) {
5239                        state.push('^');
5240                    }
5241                    if pane.is_tab_pinned(ix) {
5242                        state.push('!');
5243                    }
5244                    state
5245                })
5246                .collect::<Vec<_>>()
5247        });
5248        assert_eq!(
5249            actual_states, expected_states,
5250            "pane items do not match expectation"
5251        );
5252    }
5253}