pane.rs

   1use crate::{
   2    CloseWindow, NewFile, NewTerminal, OpenInTerminal, OpenOptions, OpenTerminal, OpenVisible,
   3    SplitDirection, ToggleFileFinder, ToggleProjectSymbols, ToggleZoom, Workspace,
   4    WorkspaceItemBuilder,
   5    item::{
   6        ActivateOnClose, ClosePosition, Item, ItemHandle, ItemSettings, PreviewTabsSettings,
   7        ProjectItemKind, ShowCloseButton, ShowDiagnostics, TabContentParams, TabTooltipContent,
   8        WeakItemHandle,
   9    },
  10    move_item,
  11    notifications::NotifyResultExt,
  12    toolbar::Toolbar,
  13    workspace_settings::{AutosaveSetting, TabBarSettings, WorkspaceSettings},
  14};
  15use anyhow::Result;
  16use collections::{BTreeSet, HashMap, HashSet, VecDeque};
  17use futures::{StreamExt, stream::FuturesUnordered};
  18use gpui::{
  19    Action, AnyElement, App, AsyncWindowContext, ClickEvent, ClipboardItem, Context, Corner, Div,
  20    DragMoveEvent, Entity, EntityId, EventEmitter, ExternalPaths, FocusHandle, FocusOutEvent,
  21    Focusable, KeyContext, MouseButton, MouseDownEvent, NavigationDirection, Pixels, Point,
  22    PromptLevel, Render, ScrollHandle, Subscription, Task, WeakEntity, WeakFocusHandle, Window,
  23    actions, anchored, deferred, impl_actions, prelude::*,
  24};
  25use itertools::Itertools;
  26use language::DiagnosticSeverity;
  27use parking_lot::Mutex;
  28use project::{Project, ProjectEntryId, ProjectPath, WorktreeId};
  29use schemars::JsonSchema;
  30use serde::Deserialize;
  31use settings::{Settings, SettingsStore};
  32use std::{
  33    any::Any,
  34    cmp, fmt, mem,
  35    ops::ControlFlow,
  36    path::PathBuf,
  37    rc::Rc,
  38    sync::{
  39        Arc,
  40        atomic::{AtomicUsize, Ordering},
  41    },
  42};
  43use theme::ThemeSettings;
  44use ui::{
  45    ButtonSize, Color, ContextMenu, ContextMenuEntry, ContextMenuItem, DecoratedIcon, IconButton,
  46    IconButtonShape, IconDecoration, IconDecorationKind, IconName, IconSize, Indicator, Label,
  47    PopoverMenu, PopoverMenuHandle, Tab, TabBar, TabPosition, Tooltip, prelude::*,
  48    right_click_menu,
  49};
  50use util::{ResultExt, debug_panic, maybe, truncate_and_remove_front};
  51
  52/// A selected entry in e.g. project panel.
  53#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
  54pub struct SelectedEntry {
  55    pub worktree_id: WorktreeId,
  56    pub entry_id: ProjectEntryId,
  57}
  58
  59/// A group of selected entries from project panel.
  60#[derive(Debug)]
  61pub struct DraggedSelection {
  62    pub active_selection: SelectedEntry,
  63    pub marked_selections: Arc<BTreeSet<SelectedEntry>>,
  64}
  65
  66impl DraggedSelection {
  67    pub fn items<'a>(&'a self) -> Box<dyn Iterator<Item = &'a SelectedEntry> + 'a> {
  68        if self.marked_selections.contains(&self.active_selection) {
  69            Box::new(self.marked_selections.iter())
  70        } else {
  71            Box::new(std::iter::once(&self.active_selection))
  72        }
  73    }
  74}
  75
  76#[derive(Clone, Copy, PartialEq, Debug, Deserialize, JsonSchema)]
  77#[serde(rename_all = "snake_case")]
  78pub enum SaveIntent {
  79    /// write all files (even if unchanged)
  80    /// prompt before overwriting on-disk changes
  81    Save,
  82    /// same as Save, but without auto formatting
  83    SaveWithoutFormat,
  84    /// write any files that have local changes
  85    /// prompt before overwriting on-disk changes
  86    SaveAll,
  87    /// always prompt for a new path
  88    SaveAs,
  89    /// prompt "you have unsaved changes" before writing
  90    Close,
  91    /// write all dirty files, don't prompt on conflict
  92    Overwrite,
  93    /// skip all save-related behavior
  94    Skip,
  95}
  96
  97#[derive(Clone, PartialEq, Debug, Deserialize, JsonSchema, Default)]
  98pub struct ActivateItem(pub usize);
  99
 100#[derive(Clone, PartialEq, Debug, Deserialize, JsonSchema, Default)]
 101#[serde(deny_unknown_fields)]
 102pub struct CloseActiveItem {
 103    pub save_intent: Option<SaveIntent>,
 104    #[serde(default)]
 105    pub close_pinned: bool,
 106}
 107
 108#[derive(Clone, PartialEq, Debug, Deserialize, JsonSchema, Default)]
 109#[serde(deny_unknown_fields)]
 110pub struct CloseInactiveItems {
 111    pub save_intent: Option<SaveIntent>,
 112    #[serde(default)]
 113    pub close_pinned: bool,
 114}
 115
 116#[derive(Clone, PartialEq, Debug, Deserialize, JsonSchema, Default)]
 117#[serde(deny_unknown_fields)]
 118pub struct CloseAllItems {
 119    pub save_intent: Option<SaveIntent>,
 120    #[serde(default)]
 121    pub close_pinned: bool,
 122}
 123
 124#[derive(Clone, PartialEq, Debug, Deserialize, JsonSchema, Default)]
 125#[serde(deny_unknown_fields)]
 126pub struct CloseCleanItems {
 127    #[serde(default)]
 128    pub close_pinned: bool,
 129}
 130
 131#[derive(Clone, PartialEq, Debug, Deserialize, JsonSchema, Default)]
 132#[serde(deny_unknown_fields)]
 133pub struct CloseItemsToTheRight {
 134    #[serde(default)]
 135    pub close_pinned: bool,
 136}
 137
 138#[derive(Clone, PartialEq, Debug, Deserialize, JsonSchema, Default)]
 139#[serde(deny_unknown_fields)]
 140pub struct CloseItemsToTheLeft {
 141    #[serde(default)]
 142    pub close_pinned: bool,
 143}
 144
 145#[derive(Clone, PartialEq, Debug, Deserialize, JsonSchema, Default)]
 146#[serde(deny_unknown_fields)]
 147pub struct RevealInProjectPanel {
 148    #[serde(skip)]
 149    pub entry_id: Option<u64>,
 150}
 151
 152#[derive(Clone, PartialEq, Debug, Deserialize, JsonSchema, Default)]
 153#[serde(deny_unknown_fields)]
 154pub struct DeploySearch {
 155    #[serde(default)]
 156    pub replace_enabled: bool,
 157}
 158
 159impl_actions!(
 160    pane,
 161    [
 162        CloseAllItems,
 163        CloseActiveItem,
 164        CloseCleanItems,
 165        CloseItemsToTheLeft,
 166        CloseItemsToTheRight,
 167        CloseInactiveItems,
 168        ActivateItem,
 169        RevealInProjectPanel,
 170        DeploySearch,
 171    ]
 172);
 173
 174actions!(
 175    pane,
 176    [
 177        ActivatePreviousItem,
 178        ActivateNextItem,
 179        ActivateLastItem,
 180        AlternateFile,
 181        GoBack,
 182        GoForward,
 183        JoinIntoNext,
 184        JoinAll,
 185        ReopenClosedItem,
 186        SplitLeft,
 187        SplitUp,
 188        SplitRight,
 189        SplitDown,
 190        SplitHorizontal,
 191        SplitVertical,
 192        SwapItemLeft,
 193        SwapItemRight,
 194        TogglePreviewTab,
 195        TogglePinTab,
 196    ]
 197);
 198
 199impl DeploySearch {
 200    pub fn find() -> Self {
 201        Self {
 202            replace_enabled: false,
 203        }
 204    }
 205}
 206
 207const MAX_NAVIGATION_HISTORY_LEN: usize = 1024;
 208
 209pub enum Event {
 210    AddItem {
 211        item: Box<dyn ItemHandle>,
 212    },
 213    ActivateItem {
 214        local: bool,
 215        focus_changed: bool,
 216    },
 217    Remove {
 218        focus_on_pane: Option<Entity<Pane>>,
 219    },
 220    RemoveItem {
 221        idx: usize,
 222    },
 223    RemovedItem {
 224        item: Box<dyn ItemHandle>,
 225    },
 226    Split(SplitDirection),
 227    JoinAll,
 228    JoinIntoNext,
 229    ChangeItemTitle,
 230    Focus,
 231    ZoomIn,
 232    ZoomOut,
 233    UserSavedItem {
 234        item: Box<dyn WeakItemHandle>,
 235        save_intent: SaveIntent,
 236    },
 237}
 238
 239impl fmt::Debug for Event {
 240    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
 241        match self {
 242            Event::AddItem { item } => f
 243                .debug_struct("AddItem")
 244                .field("item", &item.item_id())
 245                .finish(),
 246            Event::ActivateItem { local, .. } => f
 247                .debug_struct("ActivateItem")
 248                .field("local", local)
 249                .finish(),
 250            Event::Remove { .. } => f.write_str("Remove"),
 251            Event::RemoveItem { idx } => f.debug_struct("RemoveItem").field("idx", idx).finish(),
 252            Event::RemovedItem { item } => f
 253                .debug_struct("RemovedItem")
 254                .field("item", &item.item_id())
 255                .finish(),
 256            Event::Split(direction) => f
 257                .debug_struct("Split")
 258                .field("direction", direction)
 259                .finish(),
 260            Event::JoinAll => f.write_str("JoinAll"),
 261            Event::JoinIntoNext => f.write_str("JoinIntoNext"),
 262            Event::ChangeItemTitle => f.write_str("ChangeItemTitle"),
 263            Event::Focus => f.write_str("Focus"),
 264            Event::ZoomIn => f.write_str("ZoomIn"),
 265            Event::ZoomOut => f.write_str("ZoomOut"),
 266            Event::UserSavedItem { item, save_intent } => f
 267                .debug_struct("UserSavedItem")
 268                .field("item", &item.id())
 269                .field("save_intent", save_intent)
 270                .finish(),
 271        }
 272    }
 273}
 274
 275/// A container for 0 to many items that are open in the workspace.
 276/// Treats all items uniformly via the [`ItemHandle`] trait, whether it's an editor, search results multibuffer, terminal or something else,
 277/// responsible for managing item tabs, focus and zoom states and drag and drop features.
 278/// Can be split, see `PaneGroup` for more details.
 279pub struct Pane {
 280    alternate_file_items: (
 281        Option<Box<dyn WeakItemHandle>>,
 282        Option<Box<dyn WeakItemHandle>>,
 283    ),
 284    focus_handle: FocusHandle,
 285    items: Vec<Box<dyn ItemHandle>>,
 286    activation_history: Vec<ActivationHistoryEntry>,
 287    next_activation_timestamp: Arc<AtomicUsize>,
 288    zoomed: bool,
 289    was_focused: bool,
 290    active_item_index: usize,
 291    preview_item_id: Option<EntityId>,
 292    last_focus_handle_by_item: HashMap<EntityId, WeakFocusHandle>,
 293    nav_history: NavHistory,
 294    toolbar: Entity<Toolbar>,
 295    pub(crate) workspace: WeakEntity<Workspace>,
 296    project: WeakEntity<Project>,
 297    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 = "This file has changed on disk since you started editing it. Do you want to overwrite it?";
1818
1819        const DELETED_MESSAGE: &str = "This file has been deleted on disk since you started editing it. Do you want to recreate it?";
1820
1821        if save_intent == SaveIntent::Skip {
1822            return Ok(true);
1823        }
1824        let Some(item_ix) = pane
1825            .update(cx, |pane, _| pane.index_for_item(item))
1826            .ok()
1827            .flatten()
1828        else {
1829            return Ok(true);
1830        };
1831
1832        let (
1833            mut has_conflict,
1834            mut is_dirty,
1835            mut can_save,
1836            can_save_as,
1837            is_singleton,
1838            has_deleted_file,
1839        ) = cx.update(|_window, cx| {
1840            (
1841                item.has_conflict(cx),
1842                item.is_dirty(cx),
1843                item.can_save(cx),
1844                item.can_save_as(cx),
1845                item.is_singleton(cx),
1846                item.has_deleted_file(cx),
1847            )
1848        })?;
1849
1850        // when saving a single buffer, we ignore whether or not it's dirty.
1851        if save_intent == SaveIntent::Save || save_intent == SaveIntent::SaveWithoutFormat {
1852            is_dirty = true;
1853        }
1854
1855        if save_intent == SaveIntent::SaveAs {
1856            is_dirty = true;
1857            has_conflict = false;
1858            can_save = false;
1859        }
1860
1861        if save_intent == SaveIntent::Overwrite {
1862            has_conflict = false;
1863        }
1864
1865        let should_format = save_intent != SaveIntent::SaveWithoutFormat;
1866
1867        if has_conflict && can_save {
1868            if has_deleted_file && is_singleton {
1869                let answer = pane.update_in(cx, |pane, window, cx| {
1870                    pane.activate_item(item_ix, true, true, window, cx);
1871                    window.prompt(
1872                        PromptLevel::Warning,
1873                        DELETED_MESSAGE,
1874                        None,
1875                        &["Save", "Close", "Cancel"],
1876                        cx,
1877                    )
1878                })?;
1879                match answer.await {
1880                    Ok(0) => {
1881                        pane.update_in(cx, |_, window, cx| {
1882                            item.save(should_format, project, window, cx)
1883                        })?
1884                        .await?
1885                    }
1886                    Ok(1) => {
1887                        pane.update_in(cx, |pane, window, cx| {
1888                            pane.remove_item(item.item_id(), false, true, window, cx)
1889                        })?;
1890                    }
1891                    _ => return Ok(false),
1892                }
1893                return Ok(true);
1894            } else {
1895                let answer = pane.update_in(cx, |pane, window, cx| {
1896                    pane.activate_item(item_ix, true, true, window, cx);
1897                    window.prompt(
1898                        PromptLevel::Warning,
1899                        CONFLICT_MESSAGE,
1900                        None,
1901                        &["Overwrite", "Discard", "Cancel"],
1902                        cx,
1903                    )
1904                })?;
1905                match answer.await {
1906                    Ok(0) => {
1907                        pane.update_in(cx, |_, window, cx| {
1908                            item.save(should_format, project, window, cx)
1909                        })?
1910                        .await?
1911                    }
1912                    Ok(1) => {
1913                        pane.update_in(cx, |_, window, cx| item.reload(project, window, cx))?
1914                            .await?
1915                    }
1916                    _ => return Ok(false),
1917                }
1918            }
1919        } else if is_dirty && (can_save || can_save_as) {
1920            if save_intent == SaveIntent::Close {
1921                let will_autosave = cx.update(|_window, cx| {
1922                    matches!(
1923                        item.workspace_settings(cx).autosave,
1924                        AutosaveSetting::OnFocusChange | AutosaveSetting::OnWindowChange
1925                    ) && Self::can_autosave_item(item, cx)
1926                })?;
1927                if !will_autosave {
1928                    let item_id = item.item_id();
1929                    let answer_task = pane.update_in(cx, |pane, window, cx| {
1930                        if pane.save_modals_spawned.insert(item_id) {
1931                            pane.activate_item(item_ix, true, true, window, cx);
1932                            let prompt = dirty_message_for(item.project_path(cx));
1933                            Some(window.prompt(
1934                                PromptLevel::Warning,
1935                                &prompt,
1936                                None,
1937                                &["Save", "Don't Save", "Cancel"],
1938                                cx,
1939                            ))
1940                        } else {
1941                            None
1942                        }
1943                    })?;
1944                    if let Some(answer_task) = answer_task {
1945                        let answer = answer_task.await;
1946                        pane.update(cx, |pane, _| {
1947                            if !pane.save_modals_spawned.remove(&item_id) {
1948                                debug_panic!(
1949                                    "save modal was not present in spawned modals after awaiting for its answer"
1950                                )
1951                            }
1952                        })?;
1953                        match answer {
1954                            Ok(0) => {}
1955                            Ok(1) => {
1956                                // Don't save this file
1957                                pane.update_in(cx, |pane, window, cx| {
1958                                    if pane.is_tab_pinned(item_ix) && !item.can_save(cx) {
1959                                        pane.pinned_tab_count -= 1;
1960                                    }
1961                                    item.discarded(project, window, cx)
1962                                })
1963                                .log_err();
1964                                return Ok(true);
1965                            }
1966                            _ => return Ok(false), // Cancel
1967                        }
1968                    } else {
1969                        return Ok(false);
1970                    }
1971                }
1972            }
1973
1974            if can_save {
1975                pane.update_in(cx, |pane, window, cx| {
1976                    if pane.is_active_preview_item(item.item_id()) {
1977                        pane.set_preview_item_id(None, cx);
1978                    }
1979                    item.save(should_format, project, window, cx)
1980                })?
1981                .await?;
1982            } else if can_save_as && is_singleton {
1983                let abs_path = pane.update_in(cx, |pane, window, cx| {
1984                    pane.activate_item(item_ix, true, true, window, cx);
1985                    pane.workspace.update(cx, |workspace, cx| {
1986                        workspace.prompt_for_new_path(window, cx)
1987                    })
1988                })??;
1989                if let Some(abs_path) = abs_path.await.ok().flatten() {
1990                    pane.update_in(cx, |pane, window, cx| {
1991                        if let Some(item) = pane.item_for_path(abs_path.clone(), cx) {
1992                            pane.remove_item(item.item_id(), false, false, window, cx);
1993                        }
1994
1995                        item.save_as(project, abs_path, window, cx)
1996                    })?
1997                    .await?;
1998                } else {
1999                    return Ok(false);
2000                }
2001            }
2002        }
2003
2004        pane.update(cx, |_, cx| {
2005            cx.emit(Event::UserSavedItem {
2006                item: item.downgrade_item(),
2007                save_intent,
2008            });
2009            true
2010        })
2011    }
2012
2013    fn can_autosave_item(item: &dyn ItemHandle, cx: &App) -> bool {
2014        let is_deleted = item.project_entry_ids(cx).is_empty();
2015        item.is_dirty(cx) && !item.has_conflict(cx) && item.can_save(cx) && !is_deleted
2016    }
2017
2018    pub fn autosave_item(
2019        item: &dyn ItemHandle,
2020        project: Entity<Project>,
2021        window: &mut Window,
2022        cx: &mut App,
2023    ) -> Task<Result<()>> {
2024        let format = !matches!(
2025            item.workspace_settings(cx).autosave,
2026            AutosaveSetting::AfterDelay { .. }
2027        );
2028        if Self::can_autosave_item(item, cx) {
2029            item.save(format, project, window, cx)
2030        } else {
2031            Task::ready(Ok(()))
2032        }
2033    }
2034
2035    pub fn focus_active_item(&mut self, window: &mut Window, cx: &mut Context<Self>) {
2036        if let Some(active_item) = self.active_item() {
2037            let focus_handle = active_item.item_focus_handle(cx);
2038            window.focus(&focus_handle);
2039        }
2040    }
2041
2042    pub fn split(&mut self, direction: SplitDirection, cx: &mut Context<Self>) {
2043        cx.emit(Event::Split(direction));
2044    }
2045
2046    pub fn toolbar(&self) -> &Entity<Toolbar> {
2047        &self.toolbar
2048    }
2049
2050    pub fn handle_deleted_project_item(
2051        &mut self,
2052        entry_id: ProjectEntryId,
2053        window: &mut Window,
2054        cx: &mut Context<Pane>,
2055    ) -> Option<()> {
2056        let item_id = self.items().find_map(|item| {
2057            if item.is_singleton(cx) && item.project_entry_ids(cx).as_slice() == [entry_id] {
2058                Some(item.item_id())
2059            } else {
2060                None
2061            }
2062        })?;
2063
2064        self.remove_item(item_id, false, true, window, cx);
2065        self.nav_history.remove_item(item_id);
2066
2067        Some(())
2068    }
2069
2070    fn update_toolbar(&mut self, window: &mut Window, cx: &mut Context<Self>) {
2071        let active_item = self
2072            .items
2073            .get(self.active_item_index)
2074            .map(|item| item.as_ref());
2075        self.toolbar.update(cx, |toolbar, cx| {
2076            toolbar.set_active_item(active_item, window, cx);
2077        });
2078    }
2079
2080    fn update_status_bar(&mut self, window: &mut Window, cx: &mut Context<Self>) {
2081        let workspace = self.workspace.clone();
2082        let pane = cx.entity().clone();
2083
2084        window.defer(cx, move |window, cx| {
2085            let Ok(status_bar) = workspace.update(cx, |workspace, _| workspace.status_bar.clone())
2086            else {
2087                return;
2088            };
2089
2090            status_bar.update(cx, move |status_bar, cx| {
2091                status_bar.set_active_pane(&pane, window, cx);
2092            });
2093        });
2094    }
2095
2096    fn entry_abs_path(&self, entry: ProjectEntryId, cx: &App) -> Option<PathBuf> {
2097        let worktree = self
2098            .workspace
2099            .upgrade()?
2100            .read(cx)
2101            .project()
2102            .read(cx)
2103            .worktree_for_entry(entry, cx)?
2104            .read(cx);
2105        let entry = worktree.entry_for_id(entry)?;
2106        match &entry.canonical_path {
2107            Some(canonical_path) => Some(canonical_path.to_path_buf()),
2108            None => worktree.absolutize(&entry.path).ok(),
2109        }
2110    }
2111
2112    pub fn icon_color(selected: bool) -> Color {
2113        if selected {
2114            Color::Default
2115        } else {
2116            Color::Muted
2117        }
2118    }
2119
2120    fn toggle_pin_tab(&mut self, _: &TogglePinTab, window: &mut Window, cx: &mut Context<Self>) {
2121        if self.items.is_empty() {
2122            return;
2123        }
2124        let active_tab_ix = self.active_item_index();
2125        if self.is_tab_pinned(active_tab_ix) {
2126            self.unpin_tab_at(active_tab_ix, window, cx);
2127        } else {
2128            self.pin_tab_at(active_tab_ix, window, cx);
2129        }
2130    }
2131
2132    fn pin_tab_at(&mut self, ix: usize, window: &mut Window, cx: &mut Context<Self>) {
2133        maybe!({
2134            let pane = cx.entity().clone();
2135            let destination_index = self.pinned_tab_count.min(ix);
2136            self.pinned_tab_count += 1;
2137            let id = self.item_for_index(ix)?.item_id();
2138
2139            if self.is_active_preview_item(id) {
2140                self.set_preview_item_id(None, cx);
2141            }
2142
2143            self.workspace
2144                .update(cx, |_, cx| {
2145                    cx.defer_in(window, move |_, window, cx| {
2146                        move_item(&pane, &pane, id, destination_index, window, cx)
2147                    });
2148                })
2149                .ok()?;
2150
2151            Some(())
2152        });
2153    }
2154
2155    fn unpin_tab_at(&mut self, ix: usize, window: &mut Window, cx: &mut Context<Self>) {
2156        maybe!({
2157            let pane = cx.entity().clone();
2158            self.pinned_tab_count = self.pinned_tab_count.checked_sub(1)?;
2159            let destination_index = self.pinned_tab_count;
2160
2161            let id = self.item_for_index(ix)?.item_id();
2162
2163            self.workspace
2164                .update(cx, |_, cx| {
2165                    cx.defer_in(window, move |_, window, cx| {
2166                        move_item(&pane, &pane, id, destination_index, window, cx)
2167                    });
2168                })
2169                .ok()?;
2170
2171            Some(())
2172        });
2173    }
2174
2175    fn is_tab_pinned(&self, ix: usize) -> bool {
2176        self.pinned_tab_count > ix
2177    }
2178
2179    fn has_pinned_tabs(&self) -> bool {
2180        self.pinned_tab_count != 0
2181    }
2182
2183    fn has_unpinned_tabs(&self) -> bool {
2184        self.pinned_tab_count < self.items.len()
2185    }
2186
2187    fn activate_unpinned_tab(&mut self, window: &mut Window, cx: &mut Context<Self>) {
2188        if self.items.is_empty() {
2189            return;
2190        }
2191        let Some(index) = self
2192            .items()
2193            .enumerate()
2194            .find_map(|(index, _item)| (!self.is_tab_pinned(index)).then_some(index))
2195        else {
2196            return;
2197        };
2198        self.activate_item(index, true, true, window, cx);
2199    }
2200
2201    fn render_tab(
2202        &self,
2203        ix: usize,
2204        item: &dyn ItemHandle,
2205        detail: usize,
2206        focus_handle: &FocusHandle,
2207        window: &mut Window,
2208        cx: &mut Context<Pane>,
2209    ) -> impl IntoElement + use<> {
2210        let is_active = ix == self.active_item_index;
2211        let is_preview = self
2212            .preview_item_id
2213            .map(|id| id == item.item_id())
2214            .unwrap_or(false);
2215
2216        let label = item.tab_content(
2217            TabContentParams {
2218                detail: Some(detail),
2219                selected: is_active,
2220                preview: is_preview,
2221            },
2222            window,
2223            cx,
2224        );
2225
2226        let item_diagnostic = item
2227            .project_path(cx)
2228            .map_or(None, |project_path| self.diagnostics.get(&project_path));
2229
2230        let decorated_icon = item_diagnostic.map_or(None, |diagnostic| {
2231            let icon = match item.tab_icon(window, cx) {
2232                Some(icon) => icon,
2233                None => return None,
2234            };
2235
2236            let knockout_item_color = if is_active {
2237                cx.theme().colors().tab_active_background
2238            } else {
2239                cx.theme().colors().tab_bar_background
2240            };
2241
2242            let (icon_decoration, icon_color) = if matches!(diagnostic, &DiagnosticSeverity::ERROR)
2243            {
2244                (IconDecorationKind::X, Color::Error)
2245            } else {
2246                (IconDecorationKind::Triangle, Color::Warning)
2247            };
2248
2249            Some(DecoratedIcon::new(
2250                icon.size(IconSize::Small).color(Color::Muted),
2251                Some(
2252                    IconDecoration::new(icon_decoration, knockout_item_color, cx)
2253                        .color(icon_color.color(cx))
2254                        .position(Point {
2255                            x: px(-2.),
2256                            y: px(-2.),
2257                        }),
2258                ),
2259            ))
2260        });
2261
2262        let icon = if decorated_icon.is_none() {
2263            match item_diagnostic {
2264                Some(&DiagnosticSeverity::ERROR) => None,
2265                Some(&DiagnosticSeverity::WARNING) => None,
2266                _ => item
2267                    .tab_icon(window, cx)
2268                    .map(|icon| icon.color(Color::Muted)),
2269            }
2270            .map(|icon| icon.size(IconSize::Small))
2271        } else {
2272            None
2273        };
2274
2275        let settings = ItemSettings::get_global(cx);
2276        let close_side = &settings.close_position;
2277        let show_close_button = &settings.show_close_button;
2278        let indicator = render_item_indicator(item.boxed_clone(), cx);
2279        let item_id = item.item_id();
2280        let is_first_item = ix == 0;
2281        let is_last_item = ix == self.items.len() - 1;
2282        let is_pinned = self.is_tab_pinned(ix);
2283        let position_relative_to_active_item = ix.cmp(&self.active_item_index);
2284
2285        let tab = Tab::new(ix)
2286            .position(if is_first_item {
2287                TabPosition::First
2288            } else if is_last_item {
2289                TabPosition::Last
2290            } else {
2291                TabPosition::Middle(position_relative_to_active_item)
2292            })
2293            .close_side(match close_side {
2294                ClosePosition::Left => ui::TabCloseSide::Start,
2295                ClosePosition::Right => ui::TabCloseSide::End,
2296            })
2297            .toggle_state(is_active)
2298            .on_click(cx.listener(move |pane: &mut Self, _, window, cx| {
2299                pane.activate_item(ix, true, true, window, cx)
2300            }))
2301            // TODO: This should be a click listener with the middle mouse button instead of a mouse down listener.
2302            .on_mouse_down(
2303                MouseButton::Middle,
2304                cx.listener(move |pane, _event, window, cx| {
2305                    pane.close_item_by_id(item_id, SaveIntent::Close, window, cx)
2306                        .detach_and_log_err(cx);
2307                }),
2308            )
2309            .on_mouse_down(
2310                MouseButton::Left,
2311                cx.listener(move |pane, event: &MouseDownEvent, _, cx| {
2312                    if let Some(id) = pane.preview_item_id {
2313                        if id == item_id && event.click_count > 1 {
2314                            pane.set_preview_item_id(None, cx);
2315                        }
2316                    }
2317                }),
2318            )
2319            .on_drag(
2320                DraggedTab {
2321                    item: item.boxed_clone(),
2322                    pane: cx.entity().clone(),
2323                    detail,
2324                    is_active,
2325                    ix,
2326                },
2327                |tab, _, _, cx| cx.new(|_| tab.clone()),
2328            )
2329            .drag_over::<DraggedTab>(|tab, _, _, cx| {
2330                tab.bg(cx.theme().colors().drop_target_background)
2331            })
2332            .drag_over::<DraggedSelection>(|tab, _, _, cx| {
2333                tab.bg(cx.theme().colors().drop_target_background)
2334            })
2335            .when_some(self.can_drop_predicate.clone(), |this, p| {
2336                this.can_drop(move |a, window, cx| p(a, window, cx))
2337            })
2338            .on_drop(
2339                cx.listener(move |this, dragged_tab: &DraggedTab, window, cx| {
2340                    this.drag_split_direction = None;
2341                    this.handle_tab_drop(dragged_tab, ix, window, cx)
2342                }),
2343            )
2344            .on_drop(
2345                cx.listener(move |this, selection: &DraggedSelection, window, cx| {
2346                    this.drag_split_direction = None;
2347                    this.handle_dragged_selection_drop(selection, Some(ix), window, cx)
2348                }),
2349            )
2350            .on_drop(cx.listener(move |this, paths, window, cx| {
2351                this.drag_split_direction = None;
2352                this.handle_external_paths_drop(paths, window, cx)
2353            }))
2354            .when_some(item.tab_tooltip_content(cx), |tab, content| match content {
2355                TabTooltipContent::Text(text) => tab.tooltip(Tooltip::text(text.clone())),
2356                TabTooltipContent::Custom(element_fn) => {
2357                    tab.tooltip(move |window, cx| element_fn(window, cx))
2358                }
2359            })
2360            .start_slot::<Indicator>(indicator)
2361            .map(|this| {
2362                let end_slot_action: &'static dyn Action;
2363                let end_slot_tooltip_text: &'static str;
2364                let end_slot = if is_pinned {
2365                    end_slot_action = &TogglePinTab;
2366                    end_slot_tooltip_text = "Unpin Tab";
2367                    IconButton::new("unpin tab", IconName::Pin)
2368                        .shape(IconButtonShape::Square)
2369                        .icon_color(Color::Muted)
2370                        .size(ButtonSize::None)
2371                        .icon_size(IconSize::XSmall)
2372                        .on_click(cx.listener(move |pane, _, window, cx| {
2373                            pane.unpin_tab_at(ix, window, cx);
2374                        }))
2375                } else {
2376                    end_slot_action = &CloseActiveItem {
2377                        save_intent: None,
2378                        close_pinned: false,
2379                    };
2380                    end_slot_tooltip_text = "Close Tab";
2381                    match show_close_button {
2382                        ShowCloseButton::Always => IconButton::new("close tab", IconName::Close),
2383                        ShowCloseButton::Hover => {
2384                            IconButton::new("close tab", IconName::Close).visible_on_hover("")
2385                        }
2386                        ShowCloseButton::Hidden => return this,
2387                    }
2388                    .shape(IconButtonShape::Square)
2389                    .icon_color(Color::Muted)
2390                    .size(ButtonSize::None)
2391                    .icon_size(IconSize::XSmall)
2392                    .on_click(cx.listener(move |pane, _, window, cx| {
2393                        pane.close_item_by_id(item_id, SaveIntent::Close, window, cx)
2394                            .detach_and_log_err(cx);
2395                    }))
2396                }
2397                .map(|this| {
2398                    if is_active {
2399                        let focus_handle = focus_handle.clone();
2400                        this.tooltip(move |window, cx| {
2401                            Tooltip::for_action_in(
2402                                end_slot_tooltip_text,
2403                                end_slot_action,
2404                                &focus_handle,
2405                                window,
2406                                cx,
2407                            )
2408                        })
2409                    } else {
2410                        this.tooltip(Tooltip::text(end_slot_tooltip_text))
2411                    }
2412                });
2413                this.end_slot(end_slot)
2414            })
2415            .child(
2416                h_flex()
2417                    .gap_1()
2418                    .items_center()
2419                    .children(
2420                        std::iter::once(if let Some(decorated_icon) = decorated_icon {
2421                            Some(div().child(decorated_icon.into_any_element()))
2422                        } else if let Some(icon) = icon {
2423                            Some(div().child(icon.into_any_element()))
2424                        } else {
2425                            None
2426                        })
2427                        .flatten(),
2428                    )
2429                    .child(label),
2430            );
2431
2432        let single_entry_to_resolve = self.items[ix]
2433            .is_singleton(cx)
2434            .then(|| self.items[ix].project_entry_ids(cx).get(0).copied())
2435            .flatten();
2436
2437        let total_items = self.items.len();
2438        let has_items_to_left = ix > 0;
2439        let has_items_to_right = ix < total_items - 1;
2440        let is_pinned = self.is_tab_pinned(ix);
2441        let pane = cx.entity().downgrade();
2442        let menu_context = item.item_focus_handle(cx);
2443        right_click_menu(ix).trigger(tab).menu(move |window, cx| {
2444            let pane = pane.clone();
2445            let menu_context = menu_context.clone();
2446            ContextMenu::build(window, cx, move |mut menu, window, cx| {
2447                if let Some(pane) = pane.upgrade() {
2448                    menu = menu
2449                        .entry(
2450                            "Close",
2451                            Some(Box::new(CloseActiveItem {
2452                                save_intent: None,
2453                                close_pinned: true,
2454                            })),
2455                            window.handler_for(&pane, move |pane, window, cx| {
2456                                pane.close_item_by_id(item_id, SaveIntent::Close, window, cx)
2457                                    .detach_and_log_err(cx);
2458                            }),
2459                        )
2460                        .item(ContextMenuItem::Entry(
2461                            ContextMenuEntry::new("Close Others")
2462                                .action(Box::new(CloseInactiveItems {
2463                                    save_intent: None,
2464                                    close_pinned: false,
2465                                }))
2466                                .disabled(total_items == 1)
2467                                .handler(window.handler_for(&pane, move |pane, window, cx| {
2468                                    pane.close_items(window, cx, SaveIntent::Close, |id| {
2469                                        id != item_id
2470                                    })
2471                                    .detach_and_log_err(cx);
2472                                })),
2473                        ))
2474                        .separator()
2475                        .item(ContextMenuItem::Entry(
2476                            ContextMenuEntry::new("Close Left")
2477                                .action(Box::new(CloseItemsToTheLeft {
2478                                    close_pinned: false,
2479                                }))
2480                                .disabled(!has_items_to_left)
2481                                .handler(window.handler_for(&pane, move |pane, window, cx| {
2482                                    pane.close_items_to_the_left_by_id(
2483                                        item_id,
2484                                        &CloseItemsToTheLeft {
2485                                            close_pinned: false,
2486                                        },
2487                                        pane.get_non_closeable_item_ids(false),
2488                                        window,
2489                                        cx,
2490                                    )
2491                                    .detach_and_log_err(cx);
2492                                })),
2493                        ))
2494                        .item(ContextMenuItem::Entry(
2495                            ContextMenuEntry::new("Close Right")
2496                                .action(Box::new(CloseItemsToTheRight {
2497                                    close_pinned: false,
2498                                }))
2499                                .disabled(!has_items_to_right)
2500                                .handler(window.handler_for(&pane, move |pane, window, cx| {
2501                                    pane.close_items_to_the_right_by_id(
2502                                        item_id,
2503                                        &CloseItemsToTheRight {
2504                                            close_pinned: false,
2505                                        },
2506                                        pane.get_non_closeable_item_ids(false),
2507                                        window,
2508                                        cx,
2509                                    )
2510                                    .detach_and_log_err(cx);
2511                                })),
2512                        ))
2513                        .separator()
2514                        .entry(
2515                            "Close Clean",
2516                            Some(Box::new(CloseCleanItems {
2517                                close_pinned: false,
2518                            })),
2519                            window.handler_for(&pane, move |pane, window, cx| {
2520                                if let Some(task) = pane.close_clean_items(
2521                                    &CloseCleanItems {
2522                                        close_pinned: false,
2523                                    },
2524                                    window,
2525                                    cx,
2526                                ) {
2527                                    task.detach_and_log_err(cx)
2528                                }
2529                            }),
2530                        )
2531                        .entry(
2532                            "Close All",
2533                            Some(Box::new(CloseAllItems {
2534                                save_intent: None,
2535                                close_pinned: false,
2536                            })),
2537                            window.handler_for(&pane, |pane, window, cx| {
2538                                if let Some(task) = pane.close_all_items(
2539                                    &CloseAllItems {
2540                                        save_intent: None,
2541                                        close_pinned: false,
2542                                    },
2543                                    window,
2544                                    cx,
2545                                ) {
2546                                    task.detach_and_log_err(cx)
2547                                }
2548                            }),
2549                        );
2550
2551                    let pin_tab_entries = |menu: ContextMenu| {
2552                        menu.separator().map(|this| {
2553                            if is_pinned {
2554                                this.entry(
2555                                    "Unpin Tab",
2556                                    Some(TogglePinTab.boxed_clone()),
2557                                    window.handler_for(&pane, move |pane, window, cx| {
2558                                        pane.unpin_tab_at(ix, window, cx);
2559                                    }),
2560                                )
2561                            } else {
2562                                this.entry(
2563                                    "Pin Tab",
2564                                    Some(TogglePinTab.boxed_clone()),
2565                                    window.handler_for(&pane, move |pane, window, cx| {
2566                                        pane.pin_tab_at(ix, window, cx);
2567                                    }),
2568                                )
2569                            }
2570                        })
2571                    };
2572                    if let Some(entry) = single_entry_to_resolve {
2573                        let project_path = pane
2574                            .read(cx)
2575                            .item_for_entry(entry, cx)
2576                            .and_then(|item| item.project_path(cx));
2577                        let worktree = project_path.as_ref().and_then(|project_path| {
2578                            pane.read(cx)
2579                                .project
2580                                .upgrade()?
2581                                .read(cx)
2582                                .worktree_for_id(project_path.worktree_id, cx)
2583                        });
2584                        let has_relative_path = worktree.as_ref().is_some_and(|worktree| {
2585                            worktree
2586                                .read(cx)
2587                                .root_entry()
2588                                .map_or(false, |entry| entry.is_dir())
2589                        });
2590
2591                        let entry_abs_path = pane.read(cx).entry_abs_path(entry, cx);
2592                        let parent_abs_path = entry_abs_path
2593                            .as_deref()
2594                            .and_then(|abs_path| Some(abs_path.parent()?.to_path_buf()));
2595                        let relative_path = project_path
2596                            .map(|project_path| project_path.path)
2597                            .filter(|_| has_relative_path);
2598
2599                        let visible_in_project_panel = relative_path.is_some()
2600                            && worktree.is_some_and(|worktree| worktree.read(cx).is_visible());
2601
2602                        let entry_id = entry.to_proto();
2603                        menu = menu
2604                            .separator()
2605                            .when_some(entry_abs_path, |menu, abs_path| {
2606                                menu.entry(
2607                                    "Copy Path",
2608                                    Some(Box::new(zed_actions::workspace::CopyPath)),
2609                                    window.handler_for(&pane, move |_, _, cx| {
2610                                        cx.write_to_clipboard(ClipboardItem::new_string(
2611                                            abs_path.to_string_lossy().to_string(),
2612                                        ));
2613                                    }),
2614                                )
2615                            })
2616                            .when_some(relative_path, |menu, relative_path| {
2617                                menu.entry(
2618                                    "Copy Relative Path",
2619                                    Some(Box::new(zed_actions::workspace::CopyRelativePath)),
2620                                    window.handler_for(&pane, move |_, _, cx| {
2621                                        cx.write_to_clipboard(ClipboardItem::new_string(
2622                                            relative_path.to_string_lossy().to_string(),
2623                                        ));
2624                                    }),
2625                                )
2626                            })
2627                            .map(pin_tab_entries)
2628                            .separator()
2629                            .when(visible_in_project_panel, |menu| {
2630                                menu.entry(
2631                                    "Reveal In Project Panel",
2632                                    Some(Box::new(RevealInProjectPanel {
2633                                        entry_id: Some(entry_id),
2634                                    })),
2635                                    window.handler_for(&pane, move |pane, _, cx| {
2636                                        pane.project
2637                                            .update(cx, |_, cx| {
2638                                                cx.emit(project::Event::RevealInProjectPanel(
2639                                                    ProjectEntryId::from_proto(entry_id),
2640                                                ))
2641                                            })
2642                                            .ok();
2643                                    }),
2644                                )
2645                            })
2646                            .when_some(parent_abs_path, |menu, parent_abs_path| {
2647                                menu.entry(
2648                                    "Open in Terminal",
2649                                    Some(Box::new(OpenInTerminal)),
2650                                    window.handler_for(&pane, move |_, window, cx| {
2651                                        window.dispatch_action(
2652                                            OpenTerminal {
2653                                                working_directory: parent_abs_path.clone(),
2654                                            }
2655                                            .boxed_clone(),
2656                                            cx,
2657                                        );
2658                                    }),
2659                                )
2660                            });
2661                    } else {
2662                        menu = menu.map(pin_tab_entries);
2663                    }
2664                }
2665
2666                menu.context(menu_context)
2667            })
2668        })
2669    }
2670
2671    fn render_tab_bar(&mut self, window: &mut Window, cx: &mut Context<Pane>) -> impl IntoElement {
2672        let focus_handle = self.focus_handle.clone();
2673        let navigate_backward = IconButton::new("navigate_backward", IconName::ArrowLeft)
2674            .icon_size(IconSize::Small)
2675            .on_click({
2676                let entity = cx.entity().clone();
2677                move |_, window, cx| {
2678                    entity.update(cx, |pane, cx| pane.navigate_backward(window, cx))
2679                }
2680            })
2681            .disabled(!self.can_navigate_backward())
2682            .tooltip({
2683                let focus_handle = focus_handle.clone();
2684                move |window, cx| {
2685                    Tooltip::for_action_in("Go Back", &GoBack, &focus_handle, window, cx)
2686                }
2687            });
2688
2689        let navigate_forward = IconButton::new("navigate_forward", IconName::ArrowRight)
2690            .icon_size(IconSize::Small)
2691            .on_click({
2692                let entity = cx.entity().clone();
2693                move |_, window, cx| entity.update(cx, |pane, cx| pane.navigate_forward(window, cx))
2694            })
2695            .disabled(!self.can_navigate_forward())
2696            .tooltip({
2697                let focus_handle = focus_handle.clone();
2698                move |window, cx| {
2699                    Tooltip::for_action_in("Go Forward", &GoForward, &focus_handle, window, cx)
2700                }
2701            });
2702
2703        let mut tab_items = self
2704            .items
2705            .iter()
2706            .enumerate()
2707            .zip(tab_details(&self.items, cx))
2708            .map(|((ix, item), detail)| {
2709                self.render_tab(ix, &**item, detail, &focus_handle, window, cx)
2710            })
2711            .collect::<Vec<_>>();
2712        let tab_count = tab_items.len();
2713        let unpinned_tabs = tab_items.split_off(self.pinned_tab_count);
2714        let pinned_tabs = tab_items;
2715        TabBar::new("tab_bar")
2716            .when(
2717                self.display_nav_history_buttons.unwrap_or_default(),
2718                |tab_bar| {
2719                    tab_bar
2720                        .start_child(navigate_backward)
2721                        .start_child(navigate_forward)
2722                },
2723            )
2724            .map(|tab_bar| {
2725                if self.show_tab_bar_buttons {
2726                    let render_tab_buttons = self.render_tab_bar_buttons.clone();
2727                    let (left_children, right_children) = render_tab_buttons(self, window, cx);
2728                    tab_bar
2729                        .start_children(left_children)
2730                        .end_children(right_children)
2731                } else {
2732                    tab_bar
2733                }
2734            })
2735            .children(pinned_tabs.len().ne(&0).then(|| {
2736                h_flex()
2737                    .children(pinned_tabs)
2738                    .border_r_2()
2739                    .border_color(cx.theme().colors().border)
2740            }))
2741            .child(
2742                h_flex()
2743                    .id("unpinned tabs")
2744                    .overflow_x_scroll()
2745                    .w_full()
2746                    .track_scroll(&self.tab_bar_scroll_handle)
2747                    .children(unpinned_tabs)
2748                    .child(
2749                        div()
2750                            .id("tab_bar_drop_target")
2751                            .min_w_6()
2752                            // HACK: This empty child is currently necessary to force the drop target to appear
2753                            // despite us setting a min width above.
2754                            .child("")
2755                            .h_full()
2756                            .flex_grow()
2757                            .drag_over::<DraggedTab>(|bar, _, _, cx| {
2758                                bar.bg(cx.theme().colors().drop_target_background)
2759                            })
2760                            .drag_over::<DraggedSelection>(|bar, _, _, cx| {
2761                                bar.bg(cx.theme().colors().drop_target_background)
2762                            })
2763                            .on_drop(cx.listener(
2764                                move |this, dragged_tab: &DraggedTab, window, cx| {
2765                                    this.drag_split_direction = None;
2766                                    this.handle_tab_drop(dragged_tab, this.items.len(), window, cx)
2767                                },
2768                            ))
2769                            .on_drop(cx.listener(
2770                                move |this, selection: &DraggedSelection, window, cx| {
2771                                    this.drag_split_direction = None;
2772                                    this.handle_project_entry_drop(
2773                                        &selection.active_selection.entry_id,
2774                                        Some(tab_count),
2775                                        window,
2776                                        cx,
2777                                    )
2778                                },
2779                            ))
2780                            .on_drop(cx.listener(move |this, paths, window, cx| {
2781                                this.drag_split_direction = None;
2782                                this.handle_external_paths_drop(paths, window, cx)
2783                            }))
2784                            .on_click(cx.listener(move |this, event: &ClickEvent, window, cx| {
2785                                if event.up.click_count == 2 {
2786                                    window.dispatch_action(
2787                                        this.double_click_dispatch_action.boxed_clone(),
2788                                        cx,
2789                                    );
2790                                }
2791                            })),
2792                    ),
2793            )
2794    }
2795
2796    pub fn render_menu_overlay(menu: &Entity<ContextMenu>) -> Div {
2797        div().absolute().bottom_0().right_0().size_0().child(
2798            deferred(anchored().anchor(Corner::TopRight).child(menu.clone())).with_priority(1),
2799        )
2800    }
2801
2802    pub fn set_zoomed(&mut self, zoomed: bool, cx: &mut Context<Self>) {
2803        self.zoomed = zoomed;
2804        cx.notify();
2805    }
2806
2807    pub fn is_zoomed(&self) -> bool {
2808        self.zoomed
2809    }
2810
2811    fn handle_drag_move<T: 'static>(
2812        &mut self,
2813        event: &DragMoveEvent<T>,
2814        window: &mut Window,
2815        cx: &mut Context<Self>,
2816    ) {
2817        let can_split_predicate = self.can_split_predicate.take();
2818        let can_split = match &can_split_predicate {
2819            Some(can_split_predicate) => {
2820                can_split_predicate(self, event.dragged_item(), window, cx)
2821            }
2822            None => false,
2823        };
2824        self.can_split_predicate = can_split_predicate;
2825        if !can_split {
2826            return;
2827        }
2828
2829        let rect = event.bounds.size;
2830
2831        let size = event.bounds.size.width.min(event.bounds.size.height)
2832            * WorkspaceSettings::get_global(cx).drop_target_size;
2833
2834        let relative_cursor = Point::new(
2835            event.event.position.x - event.bounds.left(),
2836            event.event.position.y - event.bounds.top(),
2837        );
2838
2839        let direction = if relative_cursor.x < size
2840            || relative_cursor.x > rect.width - size
2841            || relative_cursor.y < size
2842            || relative_cursor.y > rect.height - size
2843        {
2844            [
2845                SplitDirection::Up,
2846                SplitDirection::Right,
2847                SplitDirection::Down,
2848                SplitDirection::Left,
2849            ]
2850            .iter()
2851            .min_by_key(|side| match side {
2852                SplitDirection::Up => relative_cursor.y,
2853                SplitDirection::Right => rect.width - relative_cursor.x,
2854                SplitDirection::Down => rect.height - relative_cursor.y,
2855                SplitDirection::Left => relative_cursor.x,
2856            })
2857            .cloned()
2858        } else {
2859            None
2860        };
2861
2862        if direction != self.drag_split_direction {
2863            self.drag_split_direction = direction;
2864        }
2865    }
2866
2867    fn handle_tab_drop(
2868        &mut self,
2869        dragged_tab: &DraggedTab,
2870        ix: usize,
2871        window: &mut Window,
2872        cx: &mut Context<Self>,
2873    ) {
2874        if let Some(custom_drop_handle) = self.custom_drop_handle.clone() {
2875            if let ControlFlow::Break(()) = custom_drop_handle(self, dragged_tab, window, cx) {
2876                return;
2877            }
2878        }
2879        let mut to_pane = cx.entity().clone();
2880        let split_direction = self.drag_split_direction;
2881        let item_id = dragged_tab.item.item_id();
2882        if let Some(preview_item_id) = self.preview_item_id {
2883            if item_id == preview_item_id {
2884                self.set_preview_item_id(None, cx);
2885            }
2886        }
2887
2888        let from_pane = dragged_tab.pane.clone();
2889        self.workspace
2890            .update(cx, |_, cx| {
2891                cx.defer_in(window, move |workspace, window, cx| {
2892                    if let Some(split_direction) = split_direction {
2893                        to_pane = workspace.split_pane(to_pane, split_direction, window, cx);
2894                    }
2895                    let old_ix = from_pane.read(cx).index_for_item_id(item_id);
2896                    let old_len = to_pane.read(cx).items.len();
2897                    move_item(&from_pane, &to_pane, item_id, ix, window, cx);
2898                    if to_pane == from_pane {
2899                        if let Some(old_index) = old_ix {
2900                            to_pane.update(cx, |this, _| {
2901                                if old_index < this.pinned_tab_count
2902                                    && (ix == this.items.len() || ix > this.pinned_tab_count)
2903                                {
2904                                    this.pinned_tab_count -= 1;
2905                                } else if this.has_pinned_tabs()
2906                                    && old_index >= this.pinned_tab_count
2907                                    && ix < this.pinned_tab_count
2908                                {
2909                                    this.pinned_tab_count += 1;
2910                                }
2911                            });
2912                        }
2913                    } else {
2914                        to_pane.update(cx, |this, _| {
2915                            if this.items.len() > old_len // Did we not deduplicate on drag?
2916                                && this.has_pinned_tabs()
2917                                && ix < this.pinned_tab_count
2918                            {
2919                                this.pinned_tab_count += 1;
2920                            }
2921                        });
2922                        from_pane.update(cx, |this, _| {
2923                            if let Some(index) = old_ix {
2924                                if this.pinned_tab_count > index {
2925                                    this.pinned_tab_count -= 1;
2926                                }
2927                            }
2928                        })
2929                    }
2930                });
2931            })
2932            .log_err();
2933    }
2934
2935    fn handle_dragged_selection_drop(
2936        &mut self,
2937        dragged_selection: &DraggedSelection,
2938        dragged_onto: Option<usize>,
2939        window: &mut Window,
2940        cx: &mut Context<Self>,
2941    ) {
2942        if let Some(custom_drop_handle) = self.custom_drop_handle.clone() {
2943            if let ControlFlow::Break(()) = custom_drop_handle(self, dragged_selection, window, cx)
2944            {
2945                return;
2946            }
2947        }
2948        self.handle_project_entry_drop(
2949            &dragged_selection.active_selection.entry_id,
2950            dragged_onto,
2951            window,
2952            cx,
2953        );
2954    }
2955
2956    fn handle_project_entry_drop(
2957        &mut self,
2958        project_entry_id: &ProjectEntryId,
2959        target: Option<usize>,
2960        window: &mut Window,
2961        cx: &mut Context<Self>,
2962    ) {
2963        if let Some(custom_drop_handle) = self.custom_drop_handle.clone() {
2964            if let ControlFlow::Break(()) = custom_drop_handle(self, project_entry_id, window, cx) {
2965                return;
2966            }
2967        }
2968        let mut to_pane = cx.entity().clone();
2969        let split_direction = self.drag_split_direction;
2970        let project_entry_id = *project_entry_id;
2971        self.workspace
2972            .update(cx, |_, cx| {
2973                cx.defer_in(window, move |workspace, window, cx| {
2974                    if let Some(path) = workspace
2975                        .project()
2976                        .read(cx)
2977                        .path_for_entry(project_entry_id, cx)
2978                    {
2979                        let load_path_task = workspace.load_path(path, window, cx);
2980                        cx.spawn_in(window, async move |workspace, cx| {
2981                            if let Some((project_entry_id, build_item)) =
2982                                load_path_task.await.notify_async_err(cx)
2983                            {
2984                                let (to_pane, new_item_handle) = workspace
2985                                    .update_in(cx, |workspace, window, cx| {
2986                                        if let Some(split_direction) = split_direction {
2987                                            to_pane = workspace.split_pane(
2988                                                to_pane,
2989                                                split_direction,
2990                                                window,
2991                                                cx,
2992                                            );
2993                                        }
2994                                        let new_item_handle = to_pane.update(cx, |pane, cx| {
2995                                            pane.open_item(
2996                                                project_entry_id,
2997                                                true,
2998                                                false,
2999                                                true,
3000                                                target,
3001                                                window,
3002                                                cx,
3003                                                build_item,
3004                                            )
3005                                        });
3006                                        (to_pane, new_item_handle)
3007                                    })
3008                                    .log_err()?;
3009                                to_pane
3010                                    .update_in(cx, |this, window, cx| {
3011                                        let Some(index) = this.index_for_item(&*new_item_handle)
3012                                        else {
3013                                            return;
3014                                        };
3015
3016                                        if target.map_or(false, |target| this.is_tab_pinned(target))
3017                                        {
3018                                            this.pin_tab_at(index, window, cx);
3019                                        }
3020                                    })
3021                                    .ok()?
3022                            }
3023                            Some(())
3024                        })
3025                        .detach();
3026                    };
3027                });
3028            })
3029            .log_err();
3030    }
3031
3032    fn handle_external_paths_drop(
3033        &mut self,
3034        paths: &ExternalPaths,
3035        window: &mut Window,
3036        cx: &mut Context<Self>,
3037    ) {
3038        if let Some(custom_drop_handle) = self.custom_drop_handle.clone() {
3039            if let ControlFlow::Break(()) = custom_drop_handle(self, paths, window, cx) {
3040                return;
3041            }
3042        }
3043        let mut to_pane = cx.entity().clone();
3044        let mut split_direction = self.drag_split_direction;
3045        let paths = paths.paths().to_vec();
3046        let is_remote = self
3047            .workspace
3048            .update(cx, |workspace, cx| {
3049                if workspace.project().read(cx).is_via_collab() {
3050                    workspace.show_error(
3051                        &anyhow::anyhow!("Cannot drop files on a remote project"),
3052                        cx,
3053                    );
3054                    true
3055                } else {
3056                    false
3057                }
3058            })
3059            .unwrap_or(true);
3060        if is_remote {
3061            return;
3062        }
3063
3064        self.workspace
3065            .update(cx, |workspace, cx| {
3066                let fs = Arc::clone(workspace.project().read(cx).fs());
3067                cx.spawn_in(window, async move |workspace, cx| {
3068                    let mut is_file_checks = FuturesUnordered::new();
3069                    for path in &paths {
3070                        is_file_checks.push(fs.is_file(path))
3071                    }
3072                    let mut has_files_to_open = false;
3073                    while let Some(is_file) = is_file_checks.next().await {
3074                        if is_file {
3075                            has_files_to_open = true;
3076                            break;
3077                        }
3078                    }
3079                    drop(is_file_checks);
3080                    if !has_files_to_open {
3081                        split_direction = None;
3082                    }
3083
3084                    if let Ok(open_task) = workspace.update_in(cx, |workspace, window, cx| {
3085                        if let Some(split_direction) = split_direction {
3086                            to_pane = workspace.split_pane(to_pane, split_direction, window, cx);
3087                        }
3088                        workspace.open_paths(
3089                            paths,
3090                            OpenOptions {
3091                                visible: Some(OpenVisible::OnlyDirectories),
3092                                ..Default::default()
3093                            },
3094                            Some(to_pane.downgrade()),
3095                            window,
3096                            cx,
3097                        )
3098                    }) {
3099                        let opened_items: Vec<_> = open_task.await;
3100                        _ = workspace.update(cx, |workspace, cx| {
3101                            for item in opened_items.into_iter().flatten() {
3102                                if let Err(e) = item {
3103                                    workspace.show_error(&e, cx);
3104                                }
3105                            }
3106                        });
3107                    }
3108                })
3109                .detach();
3110            })
3111            .log_err();
3112    }
3113
3114    pub fn display_nav_history_buttons(&mut self, display: Option<bool>) {
3115        self.display_nav_history_buttons = display;
3116    }
3117
3118    fn get_non_closeable_item_ids(&self, close_pinned: bool) -> Vec<EntityId> {
3119        if close_pinned {
3120            return vec![];
3121        }
3122
3123        self.items
3124            .iter()
3125            .enumerate()
3126            .filter(|(index, _item)| self.is_tab_pinned(*index))
3127            .map(|(_, item)| item.item_id())
3128            .collect()
3129    }
3130
3131    pub fn drag_split_direction(&self) -> Option<SplitDirection> {
3132        self.drag_split_direction
3133    }
3134
3135    pub fn set_zoom_out_on_close(&mut self, zoom_out_on_close: bool) {
3136        self.zoom_out_on_close = zoom_out_on_close;
3137    }
3138}
3139
3140impl Focusable for Pane {
3141    fn focus_handle(&self, _cx: &App) -> FocusHandle {
3142        self.focus_handle.clone()
3143    }
3144}
3145
3146impl Render for Pane {
3147    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
3148        let mut key_context = KeyContext::new_with_defaults();
3149        key_context.add("Pane");
3150        if self.active_item().is_none() {
3151            key_context.add("EmptyPane");
3152        }
3153
3154        let should_display_tab_bar = self.should_display_tab_bar.clone();
3155        let display_tab_bar = should_display_tab_bar(window, cx);
3156        let Some(project) = self.project.upgrade() else {
3157            return div().track_focus(&self.focus_handle(cx));
3158        };
3159        let is_local = project.read(cx).is_local();
3160
3161        v_flex()
3162            .key_context(key_context)
3163            .track_focus(&self.focus_handle(cx))
3164            .size_full()
3165            .flex_none()
3166            .overflow_hidden()
3167            .on_action(cx.listener(|pane, _: &AlternateFile, window, cx| {
3168                pane.alternate_file(window, cx);
3169            }))
3170            .on_action(
3171                cx.listener(|pane, _: &SplitLeft, _, cx| pane.split(SplitDirection::Left, cx)),
3172            )
3173            .on_action(cx.listener(|pane, _: &SplitUp, _, cx| pane.split(SplitDirection::Up, cx)))
3174            .on_action(cx.listener(|pane, _: &SplitHorizontal, _, cx| {
3175                pane.split(SplitDirection::horizontal(cx), cx)
3176            }))
3177            .on_action(cx.listener(|pane, _: &SplitVertical, _, cx| {
3178                pane.split(SplitDirection::vertical(cx), cx)
3179            }))
3180            .on_action(
3181                cx.listener(|pane, _: &SplitRight, _, cx| pane.split(SplitDirection::Right, cx)),
3182            )
3183            .on_action(
3184                cx.listener(|pane, _: &SplitDown, _, cx| pane.split(SplitDirection::Down, cx)),
3185            )
3186            .on_action(
3187                cx.listener(|pane, _: &GoBack, window, cx| pane.navigate_backward(window, cx)),
3188            )
3189            .on_action(
3190                cx.listener(|pane, _: &GoForward, window, cx| pane.navigate_forward(window, cx)),
3191            )
3192            .on_action(cx.listener(|_, _: &JoinIntoNext, _, cx| {
3193                cx.emit(Event::JoinIntoNext);
3194            }))
3195            .on_action(cx.listener(|_, _: &JoinAll, _, cx| {
3196                cx.emit(Event::JoinAll);
3197            }))
3198            .on_action(cx.listener(Pane::toggle_zoom))
3199            .on_action(
3200                cx.listener(|pane: &mut Pane, action: &ActivateItem, window, cx| {
3201                    pane.activate_item(action.0, true, true, window, cx);
3202                }),
3203            )
3204            .on_action(
3205                cx.listener(|pane: &mut Pane, _: &ActivateLastItem, window, cx| {
3206                    pane.activate_item(pane.items.len() - 1, true, true, window, cx);
3207                }),
3208            )
3209            .on_action(
3210                cx.listener(|pane: &mut Pane, _: &ActivatePreviousItem, window, cx| {
3211                    pane.activate_prev_item(true, window, cx);
3212                }),
3213            )
3214            .on_action(
3215                cx.listener(|pane: &mut Pane, _: &ActivateNextItem, window, cx| {
3216                    pane.activate_next_item(true, window, cx);
3217                }),
3218            )
3219            .on_action(
3220                cx.listener(|pane, _: &SwapItemLeft, window, cx| pane.swap_item_left(window, cx)),
3221            )
3222            .on_action(
3223                cx.listener(|pane, _: &SwapItemRight, window, cx| pane.swap_item_right(window, cx)),
3224            )
3225            .on_action(cx.listener(|pane, action, window, cx| {
3226                pane.toggle_pin_tab(action, window, cx);
3227            }))
3228            .when(PreviewTabsSettings::get_global(cx).enabled, |this| {
3229                this.on_action(cx.listener(|pane: &mut Pane, _: &TogglePreviewTab, _, cx| {
3230                    if let Some(active_item_id) = pane.active_item().map(|i| i.item_id()) {
3231                        if pane.is_active_preview_item(active_item_id) {
3232                            pane.set_preview_item_id(None, cx);
3233                        } else {
3234                            pane.set_preview_item_id(Some(active_item_id), cx);
3235                        }
3236                    }
3237                }))
3238            })
3239            .on_action(
3240                cx.listener(|pane: &mut Self, action: &CloseActiveItem, window, cx| {
3241                    if let Some(task) = pane.close_active_item(action, window, cx) {
3242                        task.detach_and_log_err(cx)
3243                    }
3244                }),
3245            )
3246            .on_action(
3247                cx.listener(|pane: &mut Self, action: &CloseInactiveItems, window, cx| {
3248                    if let Some(task) = pane.close_inactive_items(action, window, cx) {
3249                        task.detach_and_log_err(cx)
3250                    }
3251                }),
3252            )
3253            .on_action(
3254                cx.listener(|pane: &mut Self, action: &CloseCleanItems, window, cx| {
3255                    if let Some(task) = pane.close_clean_items(action, window, cx) {
3256                        task.detach_and_log_err(cx)
3257                    }
3258                }),
3259            )
3260            .on_action(cx.listener(
3261                |pane: &mut Self, action: &CloseItemsToTheLeft, window, cx| {
3262                    if let Some(task) = pane.close_items_to_the_left(action, window, cx) {
3263                        task.detach_and_log_err(cx)
3264                    }
3265                },
3266            ))
3267            .on_action(cx.listener(
3268                |pane: &mut Self, action: &CloseItemsToTheRight, window, cx| {
3269                    if let Some(task) = pane.close_items_to_the_right(action, window, cx) {
3270                        task.detach_and_log_err(cx)
3271                    }
3272                },
3273            ))
3274            .on_action(
3275                cx.listener(|pane: &mut Self, action: &CloseAllItems, window, cx| {
3276                    if let Some(task) = pane.close_all_items(action, window, cx) {
3277                        task.detach_and_log_err(cx)
3278                    }
3279                }),
3280            )
3281            .on_action(
3282                cx.listener(|pane: &mut Self, action: &CloseActiveItem, window, cx| {
3283                    if let Some(task) = pane.close_active_item(action, window, cx) {
3284                        task.detach_and_log_err(cx)
3285                    }
3286                }),
3287            )
3288            .on_action(
3289                cx.listener(|pane: &mut Self, action: &RevealInProjectPanel, _, cx| {
3290                    let entry_id = action
3291                        .entry_id
3292                        .map(ProjectEntryId::from_proto)
3293                        .or_else(|| pane.active_item()?.project_entry_ids(cx).first().copied());
3294                    if let Some(entry_id) = entry_id {
3295                        pane.project
3296                            .update(cx, |_, cx| {
3297                                cx.emit(project::Event::RevealInProjectPanel(entry_id))
3298                            })
3299                            .ok();
3300                    }
3301                }),
3302            )
3303            .when(self.active_item().is_some() && display_tab_bar, |pane| {
3304                pane.child(self.render_tab_bar(window, cx))
3305            })
3306            .child({
3307                let has_worktrees = project.read(cx).visible_worktrees(cx).next().is_some();
3308                // main content
3309                div()
3310                    .flex_1()
3311                    .relative()
3312                    .group("")
3313                    .overflow_hidden()
3314                    .on_drag_move::<DraggedTab>(cx.listener(Self::handle_drag_move))
3315                    .on_drag_move::<DraggedSelection>(cx.listener(Self::handle_drag_move))
3316                    .when(is_local, |div| {
3317                        div.on_drag_move::<ExternalPaths>(cx.listener(Self::handle_drag_move))
3318                    })
3319                    .map(|div| {
3320                        if let Some(item) = self.active_item() {
3321                            div.id("pane_placeholder")
3322                                .v_flex()
3323                                .size_full()
3324                                .overflow_hidden()
3325                                .child(self.toolbar.clone())
3326                                .child(item.to_any())
3327                        } else {
3328                            let placeholder = div
3329                                .id("pane_placeholder")
3330                                .h_flex()
3331                                .size_full()
3332                                .justify_center()
3333                                .on_click(cx.listener(
3334                                    move |this, event: &ClickEvent, window, cx| {
3335                                        if event.up.click_count == 2 {
3336                                            window.dispatch_action(
3337                                                this.double_click_dispatch_action.boxed_clone(),
3338                                                cx,
3339                                            );
3340                                        }
3341                                    },
3342                                ));
3343                            if has_worktrees {
3344                                placeholder
3345                            } else {
3346                                placeholder.child(
3347                                    Label::new("Open a file or project to get started.")
3348                                        .color(Color::Muted),
3349                                )
3350                            }
3351                        }
3352                    })
3353                    .child(
3354                        // drag target
3355                        div()
3356                            .invisible()
3357                            .absolute()
3358                            .bg(cx.theme().colors().drop_target_background)
3359                            .group_drag_over::<DraggedTab>("", |style| style.visible())
3360                            .group_drag_over::<DraggedSelection>("", |style| style.visible())
3361                            .when(is_local, |div| {
3362                                div.group_drag_over::<ExternalPaths>("", |style| style.visible())
3363                            })
3364                            .when_some(self.can_drop_predicate.clone(), |this, p| {
3365                                this.can_drop(move |a, window, cx| p(a, window, cx))
3366                            })
3367                            .on_drop(cx.listener(move |this, dragged_tab, window, cx| {
3368                                this.handle_tab_drop(
3369                                    dragged_tab,
3370                                    this.active_item_index(),
3371                                    window,
3372                                    cx,
3373                                )
3374                            }))
3375                            .on_drop(cx.listener(
3376                                move |this, selection: &DraggedSelection, window, cx| {
3377                                    this.handle_dragged_selection_drop(selection, None, window, cx)
3378                                },
3379                            ))
3380                            .on_drop(cx.listener(move |this, paths, window, cx| {
3381                                this.handle_external_paths_drop(paths, window, cx)
3382                            }))
3383                            .map(|div| {
3384                                let size = DefiniteLength::Fraction(0.5);
3385                                match self.drag_split_direction {
3386                                    None => div.top_0().right_0().bottom_0().left_0(),
3387                                    Some(SplitDirection::Up) => {
3388                                        div.top_0().left_0().right_0().h(size)
3389                                    }
3390                                    Some(SplitDirection::Down) => {
3391                                        div.left_0().bottom_0().right_0().h(size)
3392                                    }
3393                                    Some(SplitDirection::Left) => {
3394                                        div.top_0().left_0().bottom_0().w(size)
3395                                    }
3396                                    Some(SplitDirection::Right) => {
3397                                        div.top_0().bottom_0().right_0().w(size)
3398                                    }
3399                                }
3400                            }),
3401                    )
3402            })
3403            .on_mouse_down(
3404                MouseButton::Navigate(NavigationDirection::Back),
3405                cx.listener(|pane, _, window, cx| {
3406                    if let Some(workspace) = pane.workspace.upgrade() {
3407                        let pane = cx.entity().downgrade();
3408                        window.defer(cx, move |window, cx| {
3409                            workspace.update(cx, |workspace, cx| {
3410                                workspace.go_back(pane, window, cx).detach_and_log_err(cx)
3411                            })
3412                        })
3413                    }
3414                }),
3415            )
3416            .on_mouse_down(
3417                MouseButton::Navigate(NavigationDirection::Forward),
3418                cx.listener(|pane, _, window, cx| {
3419                    if let Some(workspace) = pane.workspace.upgrade() {
3420                        let pane = cx.entity().downgrade();
3421                        window.defer(cx, move |window, cx| {
3422                            workspace.update(cx, |workspace, cx| {
3423                                workspace
3424                                    .go_forward(pane, window, cx)
3425                                    .detach_and_log_err(cx)
3426                            })
3427                        })
3428                    }
3429                }),
3430            )
3431    }
3432}
3433
3434impl ItemNavHistory {
3435    pub fn push<D: 'static + Send + Any>(&mut self, data: Option<D>, cx: &mut App) {
3436        if self
3437            .item
3438            .upgrade()
3439            .is_some_and(|item| item.include_in_nav_history())
3440        {
3441            self.history
3442                .push(data, self.item.clone(), self.is_preview, cx);
3443        }
3444    }
3445
3446    pub fn pop_backward(&mut self, cx: &mut App) -> Option<NavigationEntry> {
3447        self.history.pop(NavigationMode::GoingBack, cx)
3448    }
3449
3450    pub fn pop_forward(&mut self, cx: &mut App) -> Option<NavigationEntry> {
3451        self.history.pop(NavigationMode::GoingForward, cx)
3452    }
3453}
3454
3455impl NavHistory {
3456    pub fn for_each_entry(
3457        &self,
3458        cx: &App,
3459        mut f: impl FnMut(&NavigationEntry, (ProjectPath, Option<PathBuf>)),
3460    ) {
3461        let borrowed_history = self.0.lock();
3462        borrowed_history
3463            .forward_stack
3464            .iter()
3465            .chain(borrowed_history.backward_stack.iter())
3466            .chain(borrowed_history.closed_stack.iter())
3467            .for_each(|entry| {
3468                if let Some(project_and_abs_path) =
3469                    borrowed_history.paths_by_item.get(&entry.item.id())
3470                {
3471                    f(entry, project_and_abs_path.clone());
3472                } else if let Some(item) = entry.item.upgrade() {
3473                    if let Some(path) = item.project_path(cx) {
3474                        f(entry, (path, None));
3475                    }
3476                }
3477            })
3478    }
3479
3480    pub fn set_mode(&mut self, mode: NavigationMode) {
3481        self.0.lock().mode = mode;
3482    }
3483
3484    pub fn mode(&self) -> NavigationMode {
3485        self.0.lock().mode
3486    }
3487
3488    pub fn disable(&mut self) {
3489        self.0.lock().mode = NavigationMode::Disabled;
3490    }
3491
3492    pub fn enable(&mut self) {
3493        self.0.lock().mode = NavigationMode::Normal;
3494    }
3495
3496    pub fn pop(&mut self, mode: NavigationMode, cx: &mut App) -> Option<NavigationEntry> {
3497        let mut state = self.0.lock();
3498        let entry = match mode {
3499            NavigationMode::Normal | NavigationMode::Disabled | NavigationMode::ClosingItem => {
3500                return None;
3501            }
3502            NavigationMode::GoingBack => &mut state.backward_stack,
3503            NavigationMode::GoingForward => &mut state.forward_stack,
3504            NavigationMode::ReopeningClosedItem => &mut state.closed_stack,
3505        }
3506        .pop_back();
3507        if entry.is_some() {
3508            state.did_update(cx);
3509        }
3510        entry
3511    }
3512
3513    pub fn push<D: 'static + Send + Any>(
3514        &mut self,
3515        data: Option<D>,
3516        item: Arc<dyn WeakItemHandle>,
3517        is_preview: bool,
3518        cx: &mut App,
3519    ) {
3520        let state = &mut *self.0.lock();
3521        match state.mode {
3522            NavigationMode::Disabled => {}
3523            NavigationMode::Normal | NavigationMode::ReopeningClosedItem => {
3524                if state.backward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
3525                    state.backward_stack.pop_front();
3526                }
3527                state.backward_stack.push_back(NavigationEntry {
3528                    item,
3529                    data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
3530                    timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
3531                    is_preview,
3532                });
3533                state.forward_stack.clear();
3534            }
3535            NavigationMode::GoingBack => {
3536                if state.forward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
3537                    state.forward_stack.pop_front();
3538                }
3539                state.forward_stack.push_back(NavigationEntry {
3540                    item,
3541                    data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
3542                    timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
3543                    is_preview,
3544                });
3545            }
3546            NavigationMode::GoingForward => {
3547                if state.backward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
3548                    state.backward_stack.pop_front();
3549                }
3550                state.backward_stack.push_back(NavigationEntry {
3551                    item,
3552                    data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
3553                    timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
3554                    is_preview,
3555                });
3556            }
3557            NavigationMode::ClosingItem => {
3558                if state.closed_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
3559                    state.closed_stack.pop_front();
3560                }
3561                state.closed_stack.push_back(NavigationEntry {
3562                    item,
3563                    data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
3564                    timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
3565                    is_preview,
3566                });
3567            }
3568        }
3569        state.did_update(cx);
3570    }
3571
3572    pub fn remove_item(&mut self, item_id: EntityId) {
3573        let mut state = self.0.lock();
3574        state.paths_by_item.remove(&item_id);
3575        state
3576            .backward_stack
3577            .retain(|entry| entry.item.id() != item_id);
3578        state
3579            .forward_stack
3580            .retain(|entry| entry.item.id() != item_id);
3581        state
3582            .closed_stack
3583            .retain(|entry| entry.item.id() != item_id);
3584    }
3585
3586    pub fn path_for_item(&self, item_id: EntityId) -> Option<(ProjectPath, Option<PathBuf>)> {
3587        self.0.lock().paths_by_item.get(&item_id).cloned()
3588    }
3589}
3590
3591impl NavHistoryState {
3592    pub fn did_update(&self, cx: &mut App) {
3593        if let Some(pane) = self.pane.upgrade() {
3594            cx.defer(move |cx| {
3595                pane.update(cx, |pane, cx| pane.history_updated(cx));
3596            });
3597        }
3598    }
3599}
3600
3601fn dirty_message_for(buffer_path: Option<ProjectPath>) -> String {
3602    let path = buffer_path
3603        .as_ref()
3604        .and_then(|p| {
3605            p.path
3606                .to_str()
3607                .and_then(|s| if s.is_empty() { None } else { Some(s) })
3608        })
3609        .unwrap_or("This buffer");
3610    let path = truncate_and_remove_front(path, 80);
3611    format!("{path} contains unsaved edits. Do you want to save it?")
3612}
3613
3614pub fn tab_details(items: &[Box<dyn ItemHandle>], cx: &App) -> Vec<usize> {
3615    let mut tab_details = items.iter().map(|_| 0).collect::<Vec<_>>();
3616    let mut tab_descriptions = HashMap::default();
3617    let mut done = false;
3618    while !done {
3619        done = true;
3620
3621        // Store item indices by their tab description.
3622        for (ix, (item, detail)) in items.iter().zip(&tab_details).enumerate() {
3623            if let Some(description) = item.tab_description(*detail, cx) {
3624                if *detail == 0
3625                    || Some(&description) != item.tab_description(detail - 1, cx).as_ref()
3626                {
3627                    tab_descriptions
3628                        .entry(description)
3629                        .or_insert(Vec::new())
3630                        .push(ix);
3631                }
3632            }
3633        }
3634
3635        // If two or more items have the same tab description, increase their level
3636        // of detail and try again.
3637        for (_, item_ixs) in tab_descriptions.drain() {
3638            if item_ixs.len() > 1 {
3639                done = false;
3640                for ix in item_ixs {
3641                    tab_details[ix] += 1;
3642                }
3643            }
3644        }
3645    }
3646
3647    tab_details
3648}
3649
3650pub fn render_item_indicator(item: Box<dyn ItemHandle>, cx: &App) -> Option<Indicator> {
3651    maybe!({
3652        let indicator_color = match (item.has_conflict(cx), item.is_dirty(cx)) {
3653            (true, _) => Color::Warning,
3654            (_, true) => Color::Accent,
3655            (false, false) => return None,
3656        };
3657
3658        Some(Indicator::dot().color(indicator_color))
3659    })
3660}
3661
3662impl Render for DraggedTab {
3663    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
3664        let ui_font = ThemeSettings::get_global(cx).ui_font.clone();
3665        let label = self.item.tab_content(
3666            TabContentParams {
3667                detail: Some(self.detail),
3668                selected: false,
3669                preview: false,
3670            },
3671            window,
3672            cx,
3673        );
3674        Tab::new("")
3675            .toggle_state(self.is_active)
3676            .child(label)
3677            .render(window, cx)
3678            .font(ui_font)
3679    }
3680}
3681
3682#[cfg(test)]
3683mod tests {
3684    use std::num::NonZero;
3685
3686    use super::*;
3687    use crate::item::test::{TestItem, TestProjectItem};
3688    use gpui::{TestAppContext, VisualTestContext};
3689    use project::FakeFs;
3690    use settings::SettingsStore;
3691    use theme::LoadThemes;
3692
3693    #[gpui::test]
3694    async fn test_remove_active_empty(cx: &mut TestAppContext) {
3695        init_test(cx);
3696        let fs = FakeFs::new(cx.executor());
3697
3698        let project = Project::test(fs, None, cx).await;
3699        let (workspace, cx) =
3700            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
3701        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3702
3703        pane.update_in(cx, |pane, window, cx| {
3704            assert!(
3705                pane.close_active_item(
3706                    &CloseActiveItem {
3707                        save_intent: None,
3708                        close_pinned: false
3709                    },
3710                    window,
3711                    cx
3712                )
3713                .is_none()
3714            )
3715        });
3716    }
3717
3718    #[gpui::test]
3719    async fn test_add_item_capped_to_max_tabs(cx: &mut TestAppContext) {
3720        init_test(cx);
3721        let fs = FakeFs::new(cx.executor());
3722
3723        let project = Project::test(fs, None, cx).await;
3724        let (workspace, cx) =
3725            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
3726        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3727
3728        for i in 0..7 {
3729            add_labeled_item(&pane, format!("{}", i).as_str(), false, cx);
3730        }
3731        set_max_tabs(cx, Some(5));
3732        add_labeled_item(&pane, "7", false, cx);
3733        // Remove items to respect the max tab cap.
3734        assert_item_labels(&pane, ["3", "4", "5", "6", "7*"], cx);
3735        pane.update_in(cx, |pane, window, cx| {
3736            pane.activate_item(0, false, false, window, cx);
3737        });
3738        add_labeled_item(&pane, "X", false, cx);
3739        // Respect activation order.
3740        assert_item_labels(&pane, ["3", "X*", "5", "6", "7"], cx);
3741
3742        for i in 0..7 {
3743            add_labeled_item(&pane, format!("D{}", i).as_str(), true, cx);
3744        }
3745        // Keeps dirty items, even over max tab cap.
3746        assert_item_labels(
3747            &pane,
3748            ["D0^", "D1^", "D2^", "D3^", "D4^", "D5^", "D6*^"],
3749            cx,
3750        );
3751
3752        set_max_tabs(cx, None);
3753        for i in 0..7 {
3754            add_labeled_item(&pane, format!("N{}", i).as_str(), false, cx);
3755        }
3756        // No cap when max tabs is None.
3757        assert_item_labels(
3758            &pane,
3759            [
3760                "D0^", "D1^", "D2^", "D3^", "D4^", "D5^", "D6^", "N0", "N1", "N2", "N3", "N4",
3761                "N5", "N6*",
3762            ],
3763            cx,
3764        );
3765    }
3766
3767    #[gpui::test]
3768    async fn test_add_item_with_new_item(cx: &mut TestAppContext) {
3769        init_test(cx);
3770        let fs = FakeFs::new(cx.executor());
3771
3772        let project = Project::test(fs, None, cx).await;
3773        let (workspace, cx) =
3774            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
3775        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3776
3777        // 1. Add with a destination index
3778        //   a. Add before the active item
3779        set_labeled_items(&pane, ["A", "B*", "C"], cx);
3780        pane.update_in(cx, |pane, window, cx| {
3781            pane.add_item(
3782                Box::new(cx.new(|cx| TestItem::new(cx).with_label("D"))),
3783                false,
3784                false,
3785                Some(0),
3786                window,
3787                cx,
3788            );
3789        });
3790        assert_item_labels(&pane, ["D*", "A", "B", "C"], cx);
3791
3792        //   b. Add after the active item
3793        set_labeled_items(&pane, ["A", "B*", "C"], cx);
3794        pane.update_in(cx, |pane, window, cx| {
3795            pane.add_item(
3796                Box::new(cx.new(|cx| TestItem::new(cx).with_label("D"))),
3797                false,
3798                false,
3799                Some(2),
3800                window,
3801                cx,
3802            );
3803        });
3804        assert_item_labels(&pane, ["A", "B", "D*", "C"], cx);
3805
3806        //   c. Add at the end of the item list (including off the length)
3807        set_labeled_items(&pane, ["A", "B*", "C"], cx);
3808        pane.update_in(cx, |pane, window, cx| {
3809            pane.add_item(
3810                Box::new(cx.new(|cx| TestItem::new(cx).with_label("D"))),
3811                false,
3812                false,
3813                Some(5),
3814                window,
3815                cx,
3816            );
3817        });
3818        assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
3819
3820        // 2. Add without a destination index
3821        //   a. Add with active item at the start of the item list
3822        set_labeled_items(&pane, ["A*", "B", "C"], cx);
3823        pane.update_in(cx, |pane, window, cx| {
3824            pane.add_item(
3825                Box::new(cx.new(|cx| TestItem::new(cx).with_label("D"))),
3826                false,
3827                false,
3828                None,
3829                window,
3830                cx,
3831            );
3832        });
3833        set_labeled_items(&pane, ["A", "D*", "B", "C"], cx);
3834
3835        //   b. Add with active item at the end of the item list
3836        set_labeled_items(&pane, ["A", "B", "C*"], cx);
3837        pane.update_in(cx, |pane, window, cx| {
3838            pane.add_item(
3839                Box::new(cx.new(|cx| TestItem::new(cx).with_label("D"))),
3840                false,
3841                false,
3842                None,
3843                window,
3844                cx,
3845            );
3846        });
3847        assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
3848    }
3849
3850    #[gpui::test]
3851    async fn test_add_item_with_existing_item(cx: &mut TestAppContext) {
3852        init_test(cx);
3853        let fs = FakeFs::new(cx.executor());
3854
3855        let project = Project::test(fs, None, cx).await;
3856        let (workspace, cx) =
3857            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
3858        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3859
3860        // 1. Add with a destination index
3861        //   1a. Add before the active item
3862        let [_, _, _, d] = set_labeled_items(&pane, ["A", "B*", "C", "D"], cx);
3863        pane.update_in(cx, |pane, window, cx| {
3864            pane.add_item(d, false, false, Some(0), window, cx);
3865        });
3866        assert_item_labels(&pane, ["D*", "A", "B", "C"], cx);
3867
3868        //   1b. Add after the active item
3869        let [_, _, _, d] = set_labeled_items(&pane, ["A", "B*", "C", "D"], cx);
3870        pane.update_in(cx, |pane, window, cx| {
3871            pane.add_item(d, false, false, Some(2), window, cx);
3872        });
3873        assert_item_labels(&pane, ["A", "B", "D*", "C"], cx);
3874
3875        //   1c. Add at the end of the item list (including off the length)
3876        let [a, _, _, _] = set_labeled_items(&pane, ["A", "B*", "C", "D"], cx);
3877        pane.update_in(cx, |pane, window, cx| {
3878            pane.add_item(a, false, false, Some(5), window, cx);
3879        });
3880        assert_item_labels(&pane, ["B", "C", "D", "A*"], cx);
3881
3882        //   1d. Add same item to active index
3883        let [_, b, _] = set_labeled_items(&pane, ["A", "B*", "C"], cx);
3884        pane.update_in(cx, |pane, window, cx| {
3885            pane.add_item(b, false, false, Some(1), window, cx);
3886        });
3887        assert_item_labels(&pane, ["A", "B*", "C"], cx);
3888
3889        //   1e. Add item to index after same item in last position
3890        let [_, _, c] = set_labeled_items(&pane, ["A", "B*", "C"], cx);
3891        pane.update_in(cx, |pane, window, cx| {
3892            pane.add_item(c, false, false, Some(2), window, cx);
3893        });
3894        assert_item_labels(&pane, ["A", "B", "C*"], cx);
3895
3896        // 2. Add without a destination index
3897        //   2a. Add with active item at the start of the item list
3898        let [_, _, _, d] = set_labeled_items(&pane, ["A*", "B", "C", "D"], cx);
3899        pane.update_in(cx, |pane, window, cx| {
3900            pane.add_item(d, false, false, None, window, cx);
3901        });
3902        assert_item_labels(&pane, ["A", "D*", "B", "C"], cx);
3903
3904        //   2b. Add with active item at the end of the item list
3905        let [a, _, _, _] = set_labeled_items(&pane, ["A", "B", "C", "D*"], cx);
3906        pane.update_in(cx, |pane, window, cx| {
3907            pane.add_item(a, false, false, None, window, cx);
3908        });
3909        assert_item_labels(&pane, ["B", "C", "D", "A*"], cx);
3910
3911        //   2c. Add active item to active item at end of list
3912        let [_, _, c] = set_labeled_items(&pane, ["A", "B", "C*"], cx);
3913        pane.update_in(cx, |pane, window, cx| {
3914            pane.add_item(c, false, false, None, window, cx);
3915        });
3916        assert_item_labels(&pane, ["A", "B", "C*"], cx);
3917
3918        //   2d. Add active item to active item at start of list
3919        let [a, _, _] = set_labeled_items(&pane, ["A*", "B", "C"], cx);
3920        pane.update_in(cx, |pane, window, cx| {
3921            pane.add_item(a, false, false, None, window, cx);
3922        });
3923        assert_item_labels(&pane, ["A*", "B", "C"], cx);
3924    }
3925
3926    #[gpui::test]
3927    async fn test_add_item_with_same_project_entries(cx: &mut TestAppContext) {
3928        init_test(cx);
3929        let fs = FakeFs::new(cx.executor());
3930
3931        let project = Project::test(fs, None, cx).await;
3932        let (workspace, cx) =
3933            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
3934        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3935
3936        // singleton view
3937        pane.update_in(cx, |pane, window, cx| {
3938            pane.add_item(
3939                Box::new(cx.new(|cx| {
3940                    TestItem::new(cx)
3941                        .with_singleton(true)
3942                        .with_label("buffer 1")
3943                        .with_project_items(&[TestProjectItem::new(1, "one.txt", cx)])
3944                })),
3945                false,
3946                false,
3947                None,
3948                window,
3949                cx,
3950            );
3951        });
3952        assert_item_labels(&pane, ["buffer 1*"], cx);
3953
3954        // new singleton view with the same project entry
3955        pane.update_in(cx, |pane, window, cx| {
3956            pane.add_item(
3957                Box::new(cx.new(|cx| {
3958                    TestItem::new(cx)
3959                        .with_singleton(true)
3960                        .with_label("buffer 1")
3961                        .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
3962                })),
3963                false,
3964                false,
3965                None,
3966                window,
3967                cx,
3968            );
3969        });
3970        assert_item_labels(&pane, ["buffer 1*"], cx);
3971
3972        // new singleton view with different project entry
3973        pane.update_in(cx, |pane, window, cx| {
3974            pane.add_item(
3975                Box::new(cx.new(|cx| {
3976                    TestItem::new(cx)
3977                        .with_singleton(true)
3978                        .with_label("buffer 2")
3979                        .with_project_items(&[TestProjectItem::new(2, "2.txt", cx)])
3980                })),
3981                false,
3982                false,
3983                None,
3984                window,
3985                cx,
3986            );
3987        });
3988        assert_item_labels(&pane, ["buffer 1", "buffer 2*"], cx);
3989
3990        // new multibuffer view with the same project entry
3991        pane.update_in(cx, |pane, window, cx| {
3992            pane.add_item(
3993                Box::new(cx.new(|cx| {
3994                    TestItem::new(cx)
3995                        .with_singleton(false)
3996                        .with_label("multibuffer 1")
3997                        .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
3998                })),
3999                false,
4000                false,
4001                None,
4002                window,
4003                cx,
4004            );
4005        });
4006        assert_item_labels(&pane, ["buffer 1", "buffer 2", "multibuffer 1*"], cx);
4007
4008        // another multibuffer view with the same project entry
4009        pane.update_in(cx, |pane, window, cx| {
4010            pane.add_item(
4011                Box::new(cx.new(|cx| {
4012                    TestItem::new(cx)
4013                        .with_singleton(false)
4014                        .with_label("multibuffer 1b")
4015                        .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
4016                })),
4017                false,
4018                false,
4019                None,
4020                window,
4021                cx,
4022            );
4023        });
4024        assert_item_labels(
4025            &pane,
4026            ["buffer 1", "buffer 2", "multibuffer 1", "multibuffer 1b*"],
4027            cx,
4028        );
4029    }
4030
4031    #[gpui::test]
4032    async fn test_remove_item_ordering_history(cx: &mut TestAppContext) {
4033        init_test(cx);
4034        let fs = FakeFs::new(cx.executor());
4035
4036        let project = Project::test(fs, None, cx).await;
4037        let (workspace, cx) =
4038            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4039        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
4040
4041        add_labeled_item(&pane, "A", false, cx);
4042        add_labeled_item(&pane, "B", false, cx);
4043        add_labeled_item(&pane, "C", false, cx);
4044        add_labeled_item(&pane, "D", false, cx);
4045        assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
4046
4047        pane.update_in(cx, |pane, window, cx| {
4048            pane.activate_item(1, false, false, window, cx)
4049        });
4050        add_labeled_item(&pane, "1", false, cx);
4051        assert_item_labels(&pane, ["A", "B", "1*", "C", "D"], cx);
4052
4053        pane.update_in(cx, |pane, window, cx| {
4054            pane.close_active_item(
4055                &CloseActiveItem {
4056                    save_intent: None,
4057                    close_pinned: false,
4058                },
4059                window,
4060                cx,
4061            )
4062        })
4063        .unwrap()
4064        .await
4065        .unwrap();
4066        assert_item_labels(&pane, ["A", "B*", "C", "D"], cx);
4067
4068        pane.update_in(cx, |pane, window, cx| {
4069            pane.activate_item(3, false, false, window, cx)
4070        });
4071        assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
4072
4073        pane.update_in(cx, |pane, window, cx| {
4074            pane.close_active_item(
4075                &CloseActiveItem {
4076                    save_intent: None,
4077                    close_pinned: false,
4078                },
4079                window,
4080                cx,
4081            )
4082        })
4083        .unwrap()
4084        .await
4085        .unwrap();
4086        assert_item_labels(&pane, ["A", "B*", "C"], cx);
4087
4088        pane.update_in(cx, |pane, window, cx| {
4089            pane.close_active_item(
4090                &CloseActiveItem {
4091                    save_intent: None,
4092                    close_pinned: false,
4093                },
4094                window,
4095                cx,
4096            )
4097        })
4098        .unwrap()
4099        .await
4100        .unwrap();
4101        assert_item_labels(&pane, ["A", "C*"], cx);
4102
4103        pane.update_in(cx, |pane, window, cx| {
4104            pane.close_active_item(
4105                &CloseActiveItem {
4106                    save_intent: None,
4107                    close_pinned: false,
4108                },
4109                window,
4110                cx,
4111            )
4112        })
4113        .unwrap()
4114        .await
4115        .unwrap();
4116        assert_item_labels(&pane, ["A*"], cx);
4117    }
4118
4119    #[gpui::test]
4120    async fn test_remove_item_ordering_neighbour(cx: &mut TestAppContext) {
4121        init_test(cx);
4122        cx.update_global::<SettingsStore, ()>(|s, cx| {
4123            s.update_user_settings::<ItemSettings>(cx, |s| {
4124                s.activate_on_close = Some(ActivateOnClose::Neighbour);
4125            });
4126        });
4127        let fs = FakeFs::new(cx.executor());
4128
4129        let project = Project::test(fs, None, cx).await;
4130        let (workspace, cx) =
4131            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4132        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
4133
4134        add_labeled_item(&pane, "A", false, cx);
4135        add_labeled_item(&pane, "B", false, cx);
4136        add_labeled_item(&pane, "C", false, cx);
4137        add_labeled_item(&pane, "D", false, cx);
4138        assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
4139
4140        pane.update_in(cx, |pane, window, cx| {
4141            pane.activate_item(1, false, false, window, cx)
4142        });
4143        add_labeled_item(&pane, "1", false, cx);
4144        assert_item_labels(&pane, ["A", "B", "1*", "C", "D"], cx);
4145
4146        pane.update_in(cx, |pane, window, cx| {
4147            pane.close_active_item(
4148                &CloseActiveItem {
4149                    save_intent: None,
4150                    close_pinned: false,
4151                },
4152                window,
4153                cx,
4154            )
4155        })
4156        .unwrap()
4157        .await
4158        .unwrap();
4159        assert_item_labels(&pane, ["A", "B", "C*", "D"], cx);
4160
4161        pane.update_in(cx, |pane, window, cx| {
4162            pane.activate_item(3, false, false, window, cx)
4163        });
4164        assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
4165
4166        pane.update_in(cx, |pane, window, cx| {
4167            pane.close_active_item(
4168                &CloseActiveItem {
4169                    save_intent: None,
4170                    close_pinned: false,
4171                },
4172                window,
4173                cx,
4174            )
4175        })
4176        .unwrap()
4177        .await
4178        .unwrap();
4179        assert_item_labels(&pane, ["A", "B", "C*"], cx);
4180
4181        pane.update_in(cx, |pane, window, cx| {
4182            pane.close_active_item(
4183                &CloseActiveItem {
4184                    save_intent: None,
4185                    close_pinned: false,
4186                },
4187                window,
4188                cx,
4189            )
4190        })
4191        .unwrap()
4192        .await
4193        .unwrap();
4194        assert_item_labels(&pane, ["A", "B*"], cx);
4195
4196        pane.update_in(cx, |pane, window, cx| {
4197            pane.close_active_item(
4198                &CloseActiveItem {
4199                    save_intent: None,
4200                    close_pinned: false,
4201                },
4202                window,
4203                cx,
4204            )
4205        })
4206        .unwrap()
4207        .await
4208        .unwrap();
4209        assert_item_labels(&pane, ["A*"], cx);
4210    }
4211
4212    #[gpui::test]
4213    async fn test_remove_item_ordering_left_neighbour(cx: &mut TestAppContext) {
4214        init_test(cx);
4215        cx.update_global::<SettingsStore, ()>(|s, cx| {
4216            s.update_user_settings::<ItemSettings>(cx, |s| {
4217                s.activate_on_close = Some(ActivateOnClose::LeftNeighbour);
4218            });
4219        });
4220        let fs = FakeFs::new(cx.executor());
4221
4222        let project = Project::test(fs, None, cx).await;
4223        let (workspace, cx) =
4224            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4225        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
4226
4227        add_labeled_item(&pane, "A", false, cx);
4228        add_labeled_item(&pane, "B", false, cx);
4229        add_labeled_item(&pane, "C", false, cx);
4230        add_labeled_item(&pane, "D", false, cx);
4231        assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
4232
4233        pane.update_in(cx, |pane, window, cx| {
4234            pane.activate_item(1, false, false, window, cx)
4235        });
4236        add_labeled_item(&pane, "1", false, cx);
4237        assert_item_labels(&pane, ["A", "B", "1*", "C", "D"], cx);
4238
4239        pane.update_in(cx, |pane, window, cx| {
4240            pane.close_active_item(
4241                &CloseActiveItem {
4242                    save_intent: None,
4243                    close_pinned: false,
4244                },
4245                window,
4246                cx,
4247            )
4248        })
4249        .unwrap()
4250        .await
4251        .unwrap();
4252        assert_item_labels(&pane, ["A", "B*", "C", "D"], cx);
4253
4254        pane.update_in(cx, |pane, window, cx| {
4255            pane.activate_item(3, false, false, window, cx)
4256        });
4257        assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
4258
4259        pane.update_in(cx, |pane, window, cx| {
4260            pane.close_active_item(
4261                &CloseActiveItem {
4262                    save_intent: None,
4263                    close_pinned: false,
4264                },
4265                window,
4266                cx,
4267            )
4268        })
4269        .unwrap()
4270        .await
4271        .unwrap();
4272        assert_item_labels(&pane, ["A", "B", "C*"], cx);
4273
4274        pane.update_in(cx, |pane, window, cx| {
4275            pane.activate_item(0, false, false, window, cx)
4276        });
4277        assert_item_labels(&pane, ["A*", "B", "C"], cx);
4278
4279        pane.update_in(cx, |pane, window, cx| {
4280            pane.close_active_item(
4281                &CloseActiveItem {
4282                    save_intent: None,
4283                    close_pinned: false,
4284                },
4285                window,
4286                cx,
4287            )
4288        })
4289        .unwrap()
4290        .await
4291        .unwrap();
4292        assert_item_labels(&pane, ["B*", "C"], cx);
4293
4294        pane.update_in(cx, |pane, window, cx| {
4295            pane.close_active_item(
4296                &CloseActiveItem {
4297                    save_intent: None,
4298                    close_pinned: false,
4299                },
4300                window,
4301                cx,
4302            )
4303        })
4304        .unwrap()
4305        .await
4306        .unwrap();
4307        assert_item_labels(&pane, ["C*"], cx);
4308    }
4309
4310    #[gpui::test]
4311    async fn test_close_inactive_items(cx: &mut TestAppContext) {
4312        init_test(cx);
4313        let fs = FakeFs::new(cx.executor());
4314
4315        let project = Project::test(fs, None, cx).await;
4316        let (workspace, cx) =
4317            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4318        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
4319
4320        set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx);
4321
4322        pane.update_in(cx, |pane, window, cx| {
4323            pane.close_inactive_items(
4324                &CloseInactiveItems {
4325                    save_intent: None,
4326                    close_pinned: false,
4327                },
4328                window,
4329                cx,
4330            )
4331        })
4332        .unwrap()
4333        .await
4334        .unwrap();
4335        assert_item_labels(&pane, ["C*"], cx);
4336    }
4337
4338    #[gpui::test]
4339    async fn test_close_clean_items(cx: &mut TestAppContext) {
4340        init_test(cx);
4341        let fs = FakeFs::new(cx.executor());
4342
4343        let project = Project::test(fs, None, cx).await;
4344        let (workspace, cx) =
4345            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4346        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
4347
4348        add_labeled_item(&pane, "A", true, cx);
4349        add_labeled_item(&pane, "B", false, cx);
4350        add_labeled_item(&pane, "C", true, cx);
4351        add_labeled_item(&pane, "D", false, cx);
4352        add_labeled_item(&pane, "E", false, cx);
4353        assert_item_labels(&pane, ["A^", "B", "C^", "D", "E*"], cx);
4354
4355        pane.update_in(cx, |pane, window, cx| {
4356            pane.close_clean_items(
4357                &CloseCleanItems {
4358                    close_pinned: false,
4359                },
4360                window,
4361                cx,
4362            )
4363        })
4364        .unwrap()
4365        .await
4366        .unwrap();
4367        assert_item_labels(&pane, ["A^", "C*^"], cx);
4368    }
4369
4370    #[gpui::test]
4371    async fn test_close_items_to_the_left(cx: &mut TestAppContext) {
4372        init_test(cx);
4373        let fs = FakeFs::new(cx.executor());
4374
4375        let project = Project::test(fs, None, cx).await;
4376        let (workspace, cx) =
4377            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4378        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
4379
4380        set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx);
4381
4382        pane.update_in(cx, |pane, window, cx| {
4383            pane.close_items_to_the_left(
4384                &CloseItemsToTheLeft {
4385                    close_pinned: false,
4386                },
4387                window,
4388                cx,
4389            )
4390        })
4391        .unwrap()
4392        .await
4393        .unwrap();
4394        assert_item_labels(&pane, ["C*", "D", "E"], cx);
4395    }
4396
4397    #[gpui::test]
4398    async fn test_close_items_to_the_right(cx: &mut TestAppContext) {
4399        init_test(cx);
4400        let fs = FakeFs::new(cx.executor());
4401
4402        let project = Project::test(fs, None, cx).await;
4403        let (workspace, cx) =
4404            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4405        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
4406
4407        set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx);
4408
4409        pane.update_in(cx, |pane, window, cx| {
4410            pane.close_items_to_the_right(
4411                &CloseItemsToTheRight {
4412                    close_pinned: false,
4413                },
4414                window,
4415                cx,
4416            )
4417        })
4418        .unwrap()
4419        .await
4420        .unwrap();
4421        assert_item_labels(&pane, ["A", "B", "C*"], cx);
4422    }
4423
4424    #[gpui::test]
4425    async fn test_close_all_items(cx: &mut TestAppContext) {
4426        init_test(cx);
4427        let fs = FakeFs::new(cx.executor());
4428
4429        let project = Project::test(fs, None, cx).await;
4430        let (workspace, cx) =
4431            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4432        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
4433
4434        let item_a = add_labeled_item(&pane, "A", false, cx);
4435        add_labeled_item(&pane, "B", false, cx);
4436        add_labeled_item(&pane, "C", false, cx);
4437        assert_item_labels(&pane, ["A", "B", "C*"], cx);
4438
4439        pane.update_in(cx, |pane, window, cx| {
4440            let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4441            pane.pin_tab_at(ix, window, cx);
4442            pane.close_all_items(
4443                &CloseAllItems {
4444                    save_intent: None,
4445                    close_pinned: false,
4446                },
4447                window,
4448                cx,
4449            )
4450        })
4451        .unwrap()
4452        .await
4453        .unwrap();
4454        assert_item_labels(&pane, ["A*"], cx);
4455
4456        pane.update_in(cx, |pane, window, cx| {
4457            let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4458            pane.unpin_tab_at(ix, window, cx);
4459            pane.close_all_items(
4460                &CloseAllItems {
4461                    save_intent: None,
4462                    close_pinned: false,
4463                },
4464                window,
4465                cx,
4466            )
4467        })
4468        .unwrap()
4469        .await
4470        .unwrap();
4471
4472        assert_item_labels(&pane, [], cx);
4473
4474        add_labeled_item(&pane, "A", true, cx).update(cx, |item, cx| {
4475            item.project_items
4476                .push(TestProjectItem::new_dirty(1, "A.txt", cx))
4477        });
4478        add_labeled_item(&pane, "B", true, cx).update(cx, |item, cx| {
4479            item.project_items
4480                .push(TestProjectItem::new_dirty(2, "B.txt", cx))
4481        });
4482        add_labeled_item(&pane, "C", true, cx).update(cx, |item, cx| {
4483            item.project_items
4484                .push(TestProjectItem::new_dirty(3, "C.txt", cx))
4485        });
4486        assert_item_labels(&pane, ["A^", "B^", "C*^"], cx);
4487
4488        let save = pane
4489            .update_in(cx, |pane, window, cx| {
4490                pane.close_all_items(
4491                    &CloseAllItems {
4492                        save_intent: None,
4493                        close_pinned: false,
4494                    },
4495                    window,
4496                    cx,
4497                )
4498            })
4499            .unwrap();
4500
4501        cx.executor().run_until_parked();
4502        cx.simulate_prompt_answer("Save all");
4503        save.await.unwrap();
4504        assert_item_labels(&pane, [], cx);
4505
4506        add_labeled_item(&pane, "A", true, cx);
4507        add_labeled_item(&pane, "B", true, cx);
4508        add_labeled_item(&pane, "C", true, cx);
4509        assert_item_labels(&pane, ["A^", "B^", "C*^"], cx);
4510        let save = pane
4511            .update_in(cx, |pane, window, cx| {
4512                pane.close_all_items(
4513                    &CloseAllItems {
4514                        save_intent: None,
4515                        close_pinned: false,
4516                    },
4517                    window,
4518                    cx,
4519                )
4520            })
4521            .unwrap();
4522
4523        cx.executor().run_until_parked();
4524        cx.simulate_prompt_answer("Discard all");
4525        save.await.unwrap();
4526        assert_item_labels(&pane, [], cx);
4527    }
4528
4529    #[gpui::test]
4530    async fn test_close_with_save_intent(cx: &mut TestAppContext) {
4531        init_test(cx);
4532        let fs = FakeFs::new(cx.executor());
4533
4534        let project = Project::test(fs, None, cx).await;
4535        let (workspace, cx) =
4536            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
4537        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
4538
4539        let a = cx.update(|_, cx| TestProjectItem::new_dirty(1, "A.txt", cx));
4540        let b = cx.update(|_, cx| TestProjectItem::new_dirty(1, "B.txt", cx));
4541        let c = cx.update(|_, cx| TestProjectItem::new_dirty(1, "C.txt", cx));
4542
4543        add_labeled_item(&pane, "AB", true, cx).update(cx, |item, _| {
4544            item.project_items.push(a.clone());
4545            item.project_items.push(b.clone());
4546        });
4547        add_labeled_item(&pane, "C", true, cx)
4548            .update(cx, |item, _| item.project_items.push(c.clone()));
4549        assert_item_labels(&pane, ["AB^", "C*^"], cx);
4550
4551        pane.update_in(cx, |pane, window, cx| {
4552            pane.close_all_items(
4553                &CloseAllItems {
4554                    save_intent: Some(SaveIntent::Save),
4555                    close_pinned: false,
4556                },
4557                window,
4558                cx,
4559            )
4560        })
4561        .unwrap()
4562        .await
4563        .unwrap();
4564
4565        assert_item_labels(&pane, [], cx);
4566        cx.update(|_, cx| {
4567            assert!(!a.read(cx).is_dirty);
4568            assert!(!b.read(cx).is_dirty);
4569            assert!(!c.read(cx).is_dirty);
4570        });
4571    }
4572
4573    #[gpui::test]
4574    async fn test_close_all_items_including_pinned(cx: &mut TestAppContext) {
4575        init_test(cx);
4576        let fs = FakeFs::new(cx.executor());
4577
4578        let project = Project::test(fs, None, cx).await;
4579        let (workspace, cx) =
4580            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
4581        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
4582
4583        let item_a = add_labeled_item(&pane, "A", false, cx);
4584        add_labeled_item(&pane, "B", false, cx);
4585        add_labeled_item(&pane, "C", false, cx);
4586        assert_item_labels(&pane, ["A", "B", "C*"], cx);
4587
4588        pane.update_in(cx, |pane, window, cx| {
4589            let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4590            pane.pin_tab_at(ix, window, cx);
4591            pane.close_all_items(
4592                &CloseAllItems {
4593                    save_intent: None,
4594                    close_pinned: true,
4595                },
4596                window,
4597                cx,
4598            )
4599        })
4600        .unwrap()
4601        .await
4602        .unwrap();
4603        assert_item_labels(&pane, [], cx);
4604    }
4605
4606    #[gpui::test]
4607    async fn test_close_pinned_tab_with_non_pinned_in_same_pane(cx: &mut TestAppContext) {
4608        init_test(cx);
4609        let fs = FakeFs::new(cx.executor());
4610        let project = Project::test(fs, None, cx).await;
4611        let (workspace, cx) =
4612            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
4613
4614        // Non-pinned tabs in same pane
4615        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
4616        add_labeled_item(&pane, "A", false, cx);
4617        add_labeled_item(&pane, "B", false, cx);
4618        add_labeled_item(&pane, "C", false, cx);
4619        pane.update_in(cx, |pane, window, cx| {
4620            pane.pin_tab_at(0, window, cx);
4621        });
4622        set_labeled_items(&pane, ["A*", "B", "C"], cx);
4623        pane.update_in(cx, |pane, window, cx| {
4624            pane.close_active_item(
4625                &CloseActiveItem {
4626                    save_intent: None,
4627                    close_pinned: false,
4628                },
4629                window,
4630                cx,
4631            );
4632        });
4633        // Non-pinned tab should be active
4634        assert_item_labels(&pane, ["A", "B*", "C"], cx);
4635    }
4636
4637    #[gpui::test]
4638    async fn test_close_pinned_tab_with_non_pinned_in_different_pane(cx: &mut TestAppContext) {
4639        init_test(cx);
4640        let fs = FakeFs::new(cx.executor());
4641        let project = Project::test(fs, None, cx).await;
4642        let (workspace, cx) =
4643            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
4644
4645        // No non-pinned tabs in same pane, non-pinned tabs in another pane
4646        let pane1 = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
4647        let pane2 = workspace.update_in(cx, |workspace, window, cx| {
4648            workspace.split_pane(pane1.clone(), SplitDirection::Right, window, cx)
4649        });
4650        add_labeled_item(&pane1, "A", false, cx);
4651        pane1.update_in(cx, |pane, window, cx| {
4652            pane.pin_tab_at(0, window, cx);
4653        });
4654        set_labeled_items(&pane1, ["A*"], cx);
4655        add_labeled_item(&pane2, "B", false, cx);
4656        set_labeled_items(&pane2, ["B"], cx);
4657        pane1.update_in(cx, |pane, window, cx| {
4658            pane.close_active_item(
4659                &CloseActiveItem {
4660                    save_intent: None,
4661                    close_pinned: false,
4662                },
4663                window,
4664                cx,
4665            );
4666        });
4667        //  Non-pinned tab of other pane should be active
4668        assert_item_labels(&pane2, ["B*"], cx);
4669    }
4670
4671    fn init_test(cx: &mut TestAppContext) {
4672        cx.update(|cx| {
4673            let settings_store = SettingsStore::test(cx);
4674            cx.set_global(settings_store);
4675            theme::init(LoadThemes::JustBase, cx);
4676            crate::init_settings(cx);
4677            Project::init_settings(cx);
4678        });
4679    }
4680
4681    fn set_max_tabs(cx: &mut TestAppContext, value: Option<usize>) {
4682        cx.update_global(|store: &mut SettingsStore, cx| {
4683            store.update_user_settings::<WorkspaceSettings>(cx, |settings| {
4684                settings.max_tabs = value.map(|v| NonZero::new(v).unwrap())
4685            });
4686        });
4687    }
4688
4689    fn add_labeled_item(
4690        pane: &Entity<Pane>,
4691        label: &str,
4692        is_dirty: bool,
4693        cx: &mut VisualTestContext,
4694    ) -> Box<Entity<TestItem>> {
4695        pane.update_in(cx, |pane, window, cx| {
4696            let labeled_item =
4697                Box::new(cx.new(|cx| TestItem::new(cx).with_label(label).with_dirty(is_dirty)));
4698            pane.add_item(labeled_item.clone(), false, false, None, window, cx);
4699            labeled_item
4700        })
4701    }
4702
4703    fn set_labeled_items<const COUNT: usize>(
4704        pane: &Entity<Pane>,
4705        labels: [&str; COUNT],
4706        cx: &mut VisualTestContext,
4707    ) -> [Box<Entity<TestItem>>; COUNT] {
4708        pane.update_in(cx, |pane, window, cx| {
4709            pane.items.clear();
4710            let mut active_item_index = 0;
4711
4712            let mut index = 0;
4713            let items = labels.map(|mut label| {
4714                if label.ends_with('*') {
4715                    label = label.trim_end_matches('*');
4716                    active_item_index = index;
4717                }
4718
4719                let labeled_item = Box::new(cx.new(|cx| TestItem::new(cx).with_label(label)));
4720                pane.add_item(labeled_item.clone(), false, false, None, window, cx);
4721                index += 1;
4722                labeled_item
4723            });
4724
4725            pane.activate_item(active_item_index, false, false, window, cx);
4726
4727            items
4728        })
4729    }
4730
4731    // Assert the item label, with the active item label suffixed with a '*'
4732    #[track_caller]
4733    fn assert_item_labels<const COUNT: usize>(
4734        pane: &Entity<Pane>,
4735        expected_states: [&str; COUNT],
4736        cx: &mut VisualTestContext,
4737    ) {
4738        let actual_states = pane.update(cx, |pane, cx| {
4739            pane.items
4740                .iter()
4741                .enumerate()
4742                .map(|(ix, item)| {
4743                    let mut state = item
4744                        .to_any()
4745                        .downcast::<TestItem>()
4746                        .unwrap()
4747                        .read(cx)
4748                        .label
4749                        .clone();
4750                    if ix == pane.active_item_index {
4751                        state.push('*');
4752                    }
4753                    if item.is_dirty(cx) {
4754                        state.push('^');
4755                    }
4756                    state
4757                })
4758                .collect::<Vec<_>>()
4759        });
4760        assert_eq!(
4761            actual_states, expected_states,
4762            "pane items do not match expectation"
4763        );
4764    }
4765}