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