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