pane.rs

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