pane.rs

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