pane.rs

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