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