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