pane.rs

   1use crate::{
   2    item::{
   3        ClosePosition, Item, ItemHandle, ItemSettings, PreviewTabsSettings, TabContentParams,
   4        WeakItemHandle,
   5    },
   6    notifications::NotifyResultExt,
   7    toolbar::Toolbar,
   8    workspace_settings::{AutosaveSetting, TabBarSettings, WorkspaceSettings},
   9    CloseWindow, CopyPath, CopyRelativePath, NewFile, NewTerminal, OpenInTerminal, OpenTerminal,
  10    OpenVisible, SplitDirection, ToggleFileFinder, ToggleProjectSymbols, ToggleZoom, Workspace,
  11};
  12use anyhow::Result;
  13use collections::{BTreeSet, HashMap, HashSet, VecDeque};
  14use futures::{stream::FuturesUnordered, StreamExt};
  15use git::repository::GitFileStatus;
  16use gpui::{
  17    actions, anchored, deferred, impl_actions, prelude::*, Action, AnchorCorner, AnyElement,
  18    AppContext, AsyncWindowContext, ClickEvent, ClipboardItem, Div, DragMoveEvent, EntityId,
  19    EventEmitter, ExternalPaths, FocusHandle, FocusOutEvent, FocusableView, KeyContext, Model,
  20    MouseButton, MouseDownEvent, NavigationDirection, Pixels, Point, PromptLevel, Render,
  21    ScrollHandle, Subscription, Task, View, ViewContext, VisualContext, WeakFocusHandle, WeakView,
  22    WindowContext,
  23};
  24use itertools::Itertools;
  25use parking_lot::Mutex;
  26use project::{Project, ProjectEntryId, ProjectPath, WorktreeId};
  27use serde::Deserialize;
  28use settings::{Settings, SettingsStore};
  29use std::{
  30    any::Any,
  31    cmp, fmt, mem,
  32    ops::ControlFlow,
  33    path::PathBuf,
  34    rc::Rc,
  35    sync::{
  36        atomic::{AtomicUsize, Ordering},
  37        Arc,
  38    },
  39};
  40use theme::ThemeSettings;
  41
  42use ui::{
  43    prelude::*, right_click_menu, ButtonSize, Color, IconButton, IconButtonShape, IconName,
  44    IconSize, Indicator, Label, PopoverMenu, PopoverMenuHandle, Tab, TabBar, TabPosition, Tooltip,
  45};
  46use ui::{v_flex, ContextMenu};
  47use util::{debug_panic, maybe, truncate_and_remove_front, ResultExt};
  48
  49/// A selected entry in e.g. project panel.
  50#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
  51pub struct SelectedEntry {
  52    pub worktree_id: WorktreeId,
  53    pub entry_id: ProjectEntryId,
  54}
  55
  56/// A group of selected entries from project panel.
  57#[derive(Debug)]
  58pub struct DraggedSelection {
  59    pub active_selection: SelectedEntry,
  60    pub marked_selections: Arc<BTreeSet<SelectedEntry>>,
  61}
  62
  63impl DraggedSelection {
  64    pub fn items<'a>(&'a self) -> Box<dyn Iterator<Item = &'a SelectedEntry> + 'a> {
  65        if self.marked_selections.contains(&self.active_selection) {
  66            Box::new(self.marked_selections.iter())
  67        } else {
  68            Box::new(std::iter::once(&self.active_selection))
  69        }
  70    }
  71}
  72
  73#[derive(PartialEq, Clone, Copy, Deserialize, Debug)]
  74#[serde(rename_all = "camelCase")]
  75pub enum SaveIntent {
  76    /// write all files (even if unchanged)
  77    /// prompt before overwriting on-disk changes
  78    Save,
  79    /// same as Save, but without auto formatting
  80    SaveWithoutFormat,
  81    /// write any files that have local changes
  82    /// prompt before overwriting on-disk changes
  83    SaveAll,
  84    /// always prompt for a new path
  85    SaveAs,
  86    /// prompt "you have unsaved changes" before writing
  87    Close,
  88    /// write all dirty files, don't prompt on conflict
  89    Overwrite,
  90    /// skip all save-related behavior
  91    Skip,
  92}
  93
  94#[derive(Clone, Deserialize, PartialEq, Debug)]
  95pub struct ActivateItem(pub usize);
  96
  97#[derive(Clone, PartialEq, Debug, Deserialize, Default)]
  98#[serde(rename_all = "camelCase")]
  99pub struct CloseActiveItem {
 100    pub save_intent: Option<SaveIntent>,
 101}
 102
 103#[derive(Clone, PartialEq, Debug, Deserialize, Default)]
 104#[serde(rename_all = "camelCase")]
 105pub struct CloseInactiveItems {
 106    pub save_intent: Option<SaveIntent>,
 107}
 108
 109#[derive(Clone, PartialEq, Debug, Deserialize, Default)]
 110#[serde(rename_all = "camelCase")]
 111pub struct CloseAllItems {
 112    pub save_intent: Option<SaveIntent>,
 113}
 114
 115#[derive(Clone, PartialEq, Debug, Deserialize, Default)]
 116#[serde(rename_all = "camelCase")]
 117pub struct RevealInProjectPanel {
 118    pub entry_id: Option<u64>,
 119}
 120
 121#[derive(Default, PartialEq, Clone, Deserialize)]
 122pub struct DeploySearch {
 123    #[serde(default)]
 124    pub replace_enabled: bool,
 125}
 126
 127impl_actions!(
 128    pane,
 129    [
 130        CloseAllItems,
 131        CloseActiveItem,
 132        CloseInactiveItems,
 133        ActivateItem,
 134        RevealInProjectPanel,
 135        DeploySearch,
 136    ]
 137);
 138
 139actions!(
 140    pane,
 141    [
 142        ActivatePrevItem,
 143        ActivateNextItem,
 144        ActivateLastItem,
 145        AlternateFile,
 146        CloseCleanItems,
 147        CloseItemsToTheLeft,
 148        CloseItemsToTheRight,
 149        GoBack,
 150        GoForward,
 151        JoinIntoNext,
 152        ReopenClosedItem,
 153        SplitLeft,
 154        SplitUp,
 155        SplitRight,
 156        SplitDown,
 157        SplitHorizontal,
 158        SplitVertical,
 159        TogglePreviewTab,
 160    ]
 161);
 162
 163impl DeploySearch {
 164    pub fn find() -> Self {
 165        Self {
 166            replace_enabled: false,
 167        }
 168    }
 169}
 170
 171const MAX_NAVIGATION_HISTORY_LEN: usize = 1024;
 172
 173pub enum Event {
 174    AddItem {
 175        item: Box<dyn ItemHandle>,
 176    },
 177    ActivateItem {
 178        local: bool,
 179    },
 180    Remove {
 181        focus_on_pane: Option<View<Pane>>,
 182    },
 183    RemoveItem {
 184        idx: usize,
 185    },
 186    RemovedItem {
 187        item_id: EntityId,
 188    },
 189    Split(SplitDirection),
 190    JoinIntoNext,
 191    ChangeItemTitle,
 192    Focus,
 193    ZoomIn,
 194    ZoomOut,
 195    UserSavedItem {
 196        item: Box<dyn WeakItemHandle>,
 197        save_intent: SaveIntent,
 198    },
 199}
 200
 201impl fmt::Debug for Event {
 202    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
 203        match self {
 204            Event::AddItem { item } => f
 205                .debug_struct("AddItem")
 206                .field("item", &item.item_id())
 207                .finish(),
 208            Event::ActivateItem { local } => f
 209                .debug_struct("ActivateItem")
 210                .field("local", local)
 211                .finish(),
 212            Event::Remove { .. } => f.write_str("Remove"),
 213            Event::RemoveItem { idx } => f.debug_struct("RemoveItem").field("idx", idx).finish(),
 214            Event::RemovedItem { item_id } => f
 215                .debug_struct("RemovedItem")
 216                .field("item_id", item_id)
 217                .finish(),
 218            Event::Split(direction) => f
 219                .debug_struct("Split")
 220                .field("direction", direction)
 221                .finish(),
 222            Event::JoinIntoNext => f.write_str("JoinIntoNext"),
 223            Event::ChangeItemTitle => f.write_str("ChangeItemTitle"),
 224            Event::Focus => f.write_str("Focus"),
 225            Event::ZoomIn => f.write_str("ZoomIn"),
 226            Event::ZoomOut => f.write_str("ZoomOut"),
 227            Event::UserSavedItem { item, save_intent } => f
 228                .debug_struct("UserSavedItem")
 229                .field("item", &item.id())
 230                .field("save_intent", save_intent)
 231                .finish(),
 232        }
 233    }
 234}
 235
 236/// A container for 0 to many items that are open in the workspace.
 237/// Treats all items uniformly via the [`ItemHandle`] trait, whether it's an editor, search results multibuffer, terminal or something else,
 238/// responsible for managing item tabs, focus and zoom states and drag and drop features.
 239/// Can be split, see `PaneGroup` for more details.
 240pub struct Pane {
 241    alternate_file_items: (
 242        Option<Box<dyn WeakItemHandle>>,
 243        Option<Box<dyn WeakItemHandle>>,
 244    ),
 245    focus_handle: FocusHandle,
 246    items: Vec<Box<dyn ItemHandle>>,
 247    activation_history: Vec<ActivationHistoryEntry>,
 248    next_activation_timestamp: Arc<AtomicUsize>,
 249    zoomed: bool,
 250    was_focused: bool,
 251    active_item_index: usize,
 252    preview_item_id: Option<EntityId>,
 253    last_focus_handle_by_item: HashMap<EntityId, WeakFocusHandle>,
 254    nav_history: NavHistory,
 255    toolbar: View<Toolbar>,
 256    pub(crate) workspace: WeakView<Workspace>,
 257    project: Model<Project>,
 258    drag_split_direction: Option<SplitDirection>,
 259    can_drop_predicate: Option<Arc<dyn Fn(&dyn Any, &mut WindowContext) -> bool>>,
 260    custom_drop_handle:
 261        Option<Arc<dyn Fn(&mut Pane, &dyn Any, &mut ViewContext<Pane>) -> ControlFlow<(), ()>>>,
 262    can_split: bool,
 263    should_display_tab_bar: Rc<dyn Fn(&ViewContext<Pane>) -> bool>,
 264    render_tab_bar_buttons:
 265        Rc<dyn Fn(&mut Pane, &mut ViewContext<Pane>) -> (Option<AnyElement>, Option<AnyElement>)>,
 266    _subscriptions: Vec<Subscription>,
 267    tab_bar_scroll_handle: ScrollHandle,
 268    /// Is None if navigation buttons are permanently turned off (and should not react to setting changes).
 269    /// Otherwise, when `display_nav_history_buttons` is Some, it determines whether nav buttons should be displayed.
 270    display_nav_history_buttons: Option<bool>,
 271    double_click_dispatch_action: Box<dyn Action>,
 272    save_modals_spawned: HashSet<EntityId>,
 273    pub new_item_context_menu_handle: PopoverMenuHandle<ContextMenu>,
 274    split_item_context_menu_handle: PopoverMenuHandle<ContextMenu>,
 275}
 276
 277pub struct ActivationHistoryEntry {
 278    pub entity_id: EntityId,
 279    pub timestamp: usize,
 280}
 281
 282pub struct ItemNavHistory {
 283    history: NavHistory,
 284    item: Arc<dyn WeakItemHandle>,
 285    is_preview: bool,
 286}
 287
 288#[derive(Clone)]
 289pub struct NavHistory(Arc<Mutex<NavHistoryState>>);
 290
 291struct NavHistoryState {
 292    mode: NavigationMode,
 293    backward_stack: VecDeque<NavigationEntry>,
 294    forward_stack: VecDeque<NavigationEntry>,
 295    closed_stack: VecDeque<NavigationEntry>,
 296    paths_by_item: HashMap<EntityId, (ProjectPath, Option<PathBuf>)>,
 297    pane: WeakView<Pane>,
 298    next_timestamp: Arc<AtomicUsize>,
 299}
 300
 301#[derive(Debug, Copy, Clone)]
 302pub enum NavigationMode {
 303    Normal,
 304    GoingBack,
 305    GoingForward,
 306    ClosingItem,
 307    ReopeningClosedItem,
 308    Disabled,
 309}
 310
 311impl Default for NavigationMode {
 312    fn default() -> Self {
 313        Self::Normal
 314    }
 315}
 316
 317pub struct NavigationEntry {
 318    pub item: Arc<dyn WeakItemHandle>,
 319    pub data: Option<Box<dyn Any + Send>>,
 320    pub timestamp: usize,
 321    pub is_preview: bool,
 322}
 323
 324#[derive(Clone)]
 325pub struct DraggedTab {
 326    pub pane: View<Pane>,
 327    pub item: Box<dyn ItemHandle>,
 328    pub ix: usize,
 329    pub detail: usize,
 330    pub is_active: bool,
 331}
 332
 333impl EventEmitter<Event> for Pane {}
 334
 335impl Pane {
 336    pub fn new(
 337        workspace: WeakView<Workspace>,
 338        project: Model<Project>,
 339        next_timestamp: Arc<AtomicUsize>,
 340        can_drop_predicate: Option<Arc<dyn Fn(&dyn Any, &mut WindowContext) -> bool + 'static>>,
 341        double_click_dispatch_action: Box<dyn Action>,
 342        cx: &mut ViewContext<Self>,
 343    ) -> Self {
 344        let focus_handle = cx.focus_handle();
 345
 346        let subscriptions = vec![
 347            cx.on_focus(&focus_handle, Pane::focus_in),
 348            cx.on_focus_in(&focus_handle, Pane::focus_in),
 349            cx.on_focus_out(&focus_handle, Pane::focus_out),
 350            cx.observe_global::<SettingsStore>(Self::settings_changed),
 351        ];
 352
 353        let handle = cx.view().downgrade();
 354        Self {
 355            alternate_file_items: (None, None),
 356            focus_handle,
 357            items: Vec::new(),
 358            activation_history: Vec::new(),
 359            next_activation_timestamp: next_timestamp.clone(),
 360            was_focused: false,
 361            zoomed: false,
 362            active_item_index: 0,
 363            preview_item_id: None,
 364            last_focus_handle_by_item: Default::default(),
 365            nav_history: NavHistory(Arc::new(Mutex::new(NavHistoryState {
 366                mode: NavigationMode::Normal,
 367                backward_stack: Default::default(),
 368                forward_stack: Default::default(),
 369                closed_stack: Default::default(),
 370                paths_by_item: Default::default(),
 371                pane: handle.clone(),
 372                next_timestamp,
 373            }))),
 374            toolbar: cx.new_view(|_| Toolbar::new()),
 375            tab_bar_scroll_handle: ScrollHandle::new(),
 376            drag_split_direction: None,
 377            workspace,
 378            project,
 379            can_drop_predicate,
 380            custom_drop_handle: None,
 381            can_split: true,
 382            should_display_tab_bar: Rc::new(|cx| TabBarSettings::get_global(cx).show),
 383            render_tab_bar_buttons: Rc::new(move |pane, cx| {
 384                if !pane.has_focus(cx) && !pane.context_menu_focused(cx) {
 385                    return (None, None);
 386                }
 387                // Ideally we would return a vec of elements here to pass directly to the [TabBar]'s
 388                // `end_slot`, but due to needing a view here that isn't possible.
 389                let right_children = h_flex()
 390                    // Instead we need to replicate the spacing from the [TabBar]'s `end_slot` here.
 391                    .gap(Spacing::Small.rems(cx))
 392                    .child(
 393                        PopoverMenu::new("pane-tab-bar-popover-menu")
 394                            .trigger(
 395                                IconButton::new("plus", IconName::Plus)
 396                                    .icon_size(IconSize::Small)
 397                                    .tooltip(|cx| Tooltip::text("New...", cx)),
 398                            )
 399                            .anchor(AnchorCorner::TopRight)
 400                            .with_handle(pane.new_item_context_menu_handle.clone())
 401                            .menu(move |cx| {
 402                                Some(ContextMenu::build(cx, |menu, _| {
 403                                    menu.action("New File", NewFile.boxed_clone())
 404                                        .action(
 405                                            "Open File",
 406                                            ToggleFileFinder::default().boxed_clone(),
 407                                        )
 408                                        .separator()
 409                                        .action(
 410                                            "Search Project",
 411                                            DeploySearch {
 412                                                replace_enabled: false,
 413                                            }
 414                                            .boxed_clone(),
 415                                        )
 416                                        .action(
 417                                            "Search Symbols",
 418                                            ToggleProjectSymbols.boxed_clone(),
 419                                        )
 420                                        .separator()
 421                                        .action("New Terminal", NewTerminal.boxed_clone())
 422                                }))
 423                            }),
 424                    )
 425                    .child(
 426                        PopoverMenu::new("pane-tab-bar-split")
 427                            .trigger(
 428                                IconButton::new("split", IconName::Split)
 429                                    .icon_size(IconSize::Small)
 430                                    .tooltip(|cx| Tooltip::text("Split Pane", cx)),
 431                            )
 432                            .anchor(AnchorCorner::TopRight)
 433                            .with_handle(pane.split_item_context_menu_handle.clone())
 434                            .menu(move |cx| {
 435                                ContextMenu::build(cx, |menu, _| {
 436                                    menu.action("Split Right", SplitRight.boxed_clone())
 437                                        .action("Split Left", SplitLeft.boxed_clone())
 438                                        .action("Split Up", SplitUp.boxed_clone())
 439                                        .action("Split Down", SplitDown.boxed_clone())
 440                                })
 441                                .into()
 442                            }),
 443                    )
 444                    .child({
 445                        let zoomed = pane.is_zoomed();
 446                        IconButton::new("toggle_zoom", IconName::Maximize)
 447                            .icon_size(IconSize::Small)
 448                            .selected(zoomed)
 449                            .selected_icon(IconName::Minimize)
 450                            .on_click(cx.listener(|pane, _, cx| {
 451                                pane.toggle_zoom(&crate::ToggleZoom, cx);
 452                            }))
 453                            .tooltip(move |cx| {
 454                                Tooltip::for_action(
 455                                    if zoomed { "Zoom Out" } else { "Zoom In" },
 456                                    &ToggleZoom,
 457                                    cx,
 458                                )
 459                            })
 460                    })
 461                    .into_any_element()
 462                    .into();
 463                (None, right_children)
 464            }),
 465            display_nav_history_buttons: Some(
 466                TabBarSettings::get_global(cx).show_nav_history_buttons,
 467            ),
 468            _subscriptions: subscriptions,
 469            double_click_dispatch_action,
 470            save_modals_spawned: HashSet::default(),
 471            split_item_context_menu_handle: Default::default(),
 472            new_item_context_menu_handle: Default::default(),
 473        }
 474    }
 475
 476    fn alternate_file(&mut self, cx: &mut ViewContext<Pane>) {
 477        let (_, alternative) = &self.alternate_file_items;
 478        if let Some(alternative) = alternative {
 479            let existing = self
 480                .items()
 481                .find_position(|item| item.item_id() == alternative.id());
 482            if let Some((ix, _)) = existing {
 483                self.activate_item(ix, true, true, cx);
 484            } else {
 485                if let Some(upgraded) = alternative.upgrade() {
 486                    self.add_item(upgraded, true, true, None, cx);
 487                }
 488            }
 489        }
 490    }
 491
 492    pub fn track_alternate_file_items(&mut self) {
 493        if let Some(item) = self.active_item().map(|item| item.downgrade_item()) {
 494            let (current, _) = &self.alternate_file_items;
 495            match current {
 496                Some(current) => {
 497                    if current.id() != item.id() {
 498                        self.alternate_file_items =
 499                            (Some(item), self.alternate_file_items.0.take());
 500                    }
 501                }
 502                None => {
 503                    self.alternate_file_items = (Some(item), None);
 504                }
 505            }
 506        }
 507    }
 508
 509    pub fn has_focus(&self, cx: &WindowContext) -> bool {
 510        // We not only check whether our focus handle contains focus, but also
 511        // whether the active item might have focus, because we might have just activated an item
 512        // that hasn't rendered yet.
 513        // Before the next render, we might transfer focus
 514        // to the item, and `focus_handle.contains_focus` returns false because the `active_item`
 515        // is not hooked up to us in the dispatch tree.
 516        self.focus_handle.contains_focused(cx)
 517            || self
 518                .active_item()
 519                .map_or(false, |item| item.focus_handle(cx).contains_focused(cx))
 520    }
 521
 522    fn focus_in(&mut self, cx: &mut ViewContext<Self>) {
 523        if !self.was_focused {
 524            self.was_focused = true;
 525            cx.emit(Event::Focus);
 526            cx.notify();
 527        }
 528
 529        self.toolbar.update(cx, |toolbar, cx| {
 530            toolbar.focus_changed(true, cx);
 531        });
 532
 533        if let Some(active_item) = self.active_item() {
 534            if self.focus_handle.is_focused(cx) {
 535                // Pane was focused directly. We need to either focus a view inside the active item,
 536                // or focus the active item itself
 537                if let Some(weak_last_focus_handle) =
 538                    self.last_focus_handle_by_item.get(&active_item.item_id())
 539                {
 540                    if let Some(focus_handle) = weak_last_focus_handle.upgrade() {
 541                        focus_handle.focus(cx);
 542                        return;
 543                    }
 544                }
 545
 546                active_item.focus_handle(cx).focus(cx);
 547            } else if let Some(focused) = cx.focused() {
 548                if !self.context_menu_focused(cx) {
 549                    self.last_focus_handle_by_item
 550                        .insert(active_item.item_id(), focused.downgrade());
 551                }
 552            }
 553        }
 554    }
 555
 556    pub fn context_menu_focused(&self, cx: &mut ViewContext<Self>) -> bool {
 557        self.new_item_context_menu_handle.is_focused(cx)
 558            || self.split_item_context_menu_handle.is_focused(cx)
 559    }
 560
 561    fn focus_out(&mut self, _event: FocusOutEvent, cx: &mut ViewContext<Self>) {
 562        self.was_focused = false;
 563        self.toolbar.update(cx, |toolbar, cx| {
 564            toolbar.focus_changed(false, cx);
 565        });
 566        cx.notify();
 567    }
 568
 569    fn settings_changed(&mut self, cx: &mut ViewContext<Self>) {
 570        if let Some(display_nav_history_buttons) = self.display_nav_history_buttons.as_mut() {
 571            *display_nav_history_buttons = TabBarSettings::get_global(cx).show_nav_history_buttons;
 572        }
 573        if !PreviewTabsSettings::get_global(cx).enabled {
 574            self.preview_item_id = None;
 575        }
 576        cx.notify();
 577    }
 578
 579    pub fn active_item_index(&self) -> usize {
 580        self.active_item_index
 581    }
 582
 583    pub fn activation_history(&self) -> &[ActivationHistoryEntry] {
 584        &self.activation_history
 585    }
 586
 587    pub fn set_should_display_tab_bar<F>(&mut self, should_display_tab_bar: F)
 588    where
 589        F: 'static + Fn(&ViewContext<Pane>) -> bool,
 590    {
 591        self.should_display_tab_bar = Rc::new(should_display_tab_bar);
 592    }
 593
 594    pub fn set_can_split(&mut self, can_split: bool, cx: &mut ViewContext<Self>) {
 595        self.can_split = can_split;
 596        cx.notify();
 597    }
 598
 599    pub fn set_can_navigate(&mut self, can_navigate: bool, cx: &mut ViewContext<Self>) {
 600        self.toolbar.update(cx, |toolbar, cx| {
 601            toolbar.set_can_navigate(can_navigate, cx);
 602        });
 603        cx.notify();
 604    }
 605
 606    pub fn set_render_tab_bar_buttons<F>(&mut self, cx: &mut ViewContext<Self>, render: F)
 607    where
 608        F: 'static
 609            + Fn(&mut Pane, &mut ViewContext<Pane>) -> (Option<AnyElement>, Option<AnyElement>),
 610    {
 611        self.render_tab_bar_buttons = Rc::new(render);
 612        cx.notify();
 613    }
 614
 615    pub fn set_custom_drop_handle<F>(&mut self, cx: &mut ViewContext<Self>, handle: F)
 616    where
 617        F: 'static + Fn(&mut Pane, &dyn Any, &mut ViewContext<Pane>) -> ControlFlow<(), ()>,
 618    {
 619        self.custom_drop_handle = Some(Arc::new(handle));
 620        cx.notify();
 621    }
 622
 623    pub fn nav_history_for_item<T: Item>(&self, item: &View<T>) -> ItemNavHistory {
 624        ItemNavHistory {
 625            history: self.nav_history.clone(),
 626            item: Arc::new(item.downgrade()),
 627            is_preview: self.preview_item_id == Some(item.item_id()),
 628        }
 629    }
 630
 631    pub fn nav_history(&self) -> &NavHistory {
 632        &self.nav_history
 633    }
 634
 635    pub fn nav_history_mut(&mut self) -> &mut NavHistory {
 636        &mut self.nav_history
 637    }
 638
 639    pub fn disable_history(&mut self) {
 640        self.nav_history.disable();
 641    }
 642
 643    pub fn enable_history(&mut self) {
 644        self.nav_history.enable();
 645    }
 646
 647    pub fn can_navigate_backward(&self) -> bool {
 648        !self.nav_history.0.lock().backward_stack.is_empty()
 649    }
 650
 651    pub fn can_navigate_forward(&self) -> bool {
 652        !self.nav_history.0.lock().forward_stack.is_empty()
 653    }
 654
 655    fn navigate_backward(&mut self, cx: &mut ViewContext<Self>) {
 656        if let Some(workspace) = self.workspace.upgrade() {
 657            let pane = cx.view().downgrade();
 658            cx.window_context().defer(move |cx| {
 659                workspace.update(cx, |workspace, cx| {
 660                    workspace.go_back(pane, cx).detach_and_log_err(cx)
 661                })
 662            })
 663        }
 664    }
 665
 666    fn navigate_forward(&mut self, cx: &mut ViewContext<Self>) {
 667        if let Some(workspace) = self.workspace.upgrade() {
 668            let pane = cx.view().downgrade();
 669            cx.window_context().defer(move |cx| {
 670                workspace.update(cx, |workspace, cx| {
 671                    workspace.go_forward(pane, cx).detach_and_log_err(cx)
 672                })
 673            })
 674        }
 675    }
 676
 677    fn join_into_next(&mut self, cx: &mut ViewContext<Self>) {
 678        cx.emit(Event::JoinIntoNext);
 679    }
 680
 681    fn history_updated(&mut self, cx: &mut ViewContext<Self>) {
 682        self.toolbar.update(cx, |_, cx| cx.notify());
 683    }
 684
 685    pub fn preview_item_id(&self) -> Option<EntityId> {
 686        self.preview_item_id
 687    }
 688
 689    pub fn preview_item(&self) -> Option<Box<dyn ItemHandle>> {
 690        self.preview_item_id
 691            .and_then(|id| self.items.iter().find(|item| item.item_id() == id))
 692            .cloned()
 693    }
 694
 695    fn preview_item_idx(&self) -> Option<usize> {
 696        if let Some(preview_item_id) = self.preview_item_id {
 697            self.items
 698                .iter()
 699                .position(|item| item.item_id() == preview_item_id)
 700        } else {
 701            None
 702        }
 703    }
 704
 705    pub fn is_active_preview_item(&self, item_id: EntityId) -> bool {
 706        self.preview_item_id == Some(item_id)
 707    }
 708
 709    /// Marks the item with the given ID as the preview item.
 710    /// This will be ignored if the global setting `preview_tabs` is disabled.
 711    pub fn set_preview_item_id(&mut self, item_id: Option<EntityId>, cx: &AppContext) {
 712        if PreviewTabsSettings::get_global(cx).enabled {
 713            self.preview_item_id = item_id;
 714        }
 715    }
 716
 717    pub fn handle_item_edit(&mut self, item_id: EntityId, cx: &AppContext) {
 718        if let Some(preview_item) = self.preview_item() {
 719            if preview_item.item_id() == item_id && !preview_item.preserve_preview(cx) {
 720                self.set_preview_item_id(None, cx);
 721            }
 722        }
 723    }
 724
 725    pub(crate) fn open_item(
 726        &mut self,
 727        project_entry_id: Option<ProjectEntryId>,
 728        focus_item: bool,
 729        allow_preview: bool,
 730        cx: &mut ViewContext<Self>,
 731        build_item: impl FnOnce(&mut ViewContext<Pane>) -> Box<dyn ItemHandle>,
 732    ) -> Box<dyn ItemHandle> {
 733        let mut existing_item = None;
 734        if let Some(project_entry_id) = project_entry_id {
 735            for (index, item) in self.items.iter().enumerate() {
 736                if item.is_singleton(cx)
 737                    && item.project_entry_ids(cx).as_slice() == [project_entry_id]
 738                {
 739                    let item = item.boxed_clone();
 740                    existing_item = Some((index, item));
 741                    break;
 742                }
 743            }
 744        }
 745
 746        if let Some((index, existing_item)) = existing_item {
 747            // If the item is already open, and the item is a preview item
 748            // and we are not allowing items to open as preview, mark the item as persistent.
 749            if let Some(preview_item_id) = self.preview_item_id {
 750                if let Some(tab) = self.items.get(index) {
 751                    if tab.item_id() == preview_item_id && !allow_preview {
 752                        self.set_preview_item_id(None, cx);
 753                    }
 754                }
 755            }
 756
 757            self.activate_item(index, focus_item, focus_item, cx);
 758            existing_item
 759        } else {
 760            // If the item is being opened as preview and we have an existing preview tab,
 761            // open the new item in the position of the existing preview tab.
 762            let destination_index = if allow_preview {
 763                self.close_current_preview_item(cx)
 764            } else {
 765                None
 766            };
 767
 768            let new_item = build_item(cx);
 769
 770            if allow_preview {
 771                self.set_preview_item_id(Some(new_item.item_id()), cx);
 772            }
 773
 774            self.add_item(new_item.clone(), true, focus_item, destination_index, cx);
 775
 776            new_item
 777        }
 778    }
 779
 780    pub fn close_current_preview_item(&mut self, cx: &mut ViewContext<Self>) -> Option<usize> {
 781        let Some(item_idx) = self.preview_item_idx() else {
 782            return None;
 783        };
 784
 785        let prev_active_item_index = self.active_item_index;
 786        self.remove_item(item_idx, false, false, cx);
 787        self.active_item_index = prev_active_item_index;
 788
 789        if item_idx < self.items.len() {
 790            Some(item_idx)
 791        } else {
 792            None
 793        }
 794    }
 795
 796    pub fn add_item(
 797        &mut self,
 798        item: Box<dyn ItemHandle>,
 799        activate_pane: bool,
 800        focus_item: bool,
 801        destination_index: Option<usize>,
 802        cx: &mut ViewContext<Self>,
 803    ) {
 804        if item.is_singleton(cx) {
 805            if let Some(&entry_id) = item.project_entry_ids(cx).get(0) {
 806                let project = self.project.read(cx);
 807                if let Some(project_path) = project.path_for_entry(entry_id, cx) {
 808                    let abs_path = project.absolute_path(&project_path, cx);
 809                    self.nav_history
 810                        .0
 811                        .lock()
 812                        .paths_by_item
 813                        .insert(item.item_id(), (project_path, abs_path));
 814                }
 815            }
 816        }
 817        // If no destination index is specified, add or move the item after the active item.
 818        let mut insertion_index = {
 819            cmp::min(
 820                if let Some(destination_index) = destination_index {
 821                    destination_index
 822                } else {
 823                    self.active_item_index + 1
 824                },
 825                self.items.len(),
 826            )
 827        };
 828
 829        // Does the item already exist?
 830        let project_entry_id = if item.is_singleton(cx) {
 831            item.project_entry_ids(cx).get(0).copied()
 832        } else {
 833            None
 834        };
 835
 836        let existing_item_index = self.items.iter().position(|existing_item| {
 837            if existing_item.item_id() == item.item_id() {
 838                true
 839            } else if existing_item.is_singleton(cx) {
 840                existing_item
 841                    .project_entry_ids(cx)
 842                    .get(0)
 843                    .map_or(false, |existing_entry_id| {
 844                        Some(existing_entry_id) == project_entry_id.as_ref()
 845                    })
 846            } else {
 847                false
 848            }
 849        });
 850
 851        if let Some(existing_item_index) = existing_item_index {
 852            // If the item already exists, move it to the desired destination and activate it
 853
 854            if existing_item_index != insertion_index {
 855                let existing_item_is_active = existing_item_index == self.active_item_index;
 856
 857                // If the caller didn't specify a destination and the added item is already
 858                // the active one, don't move it
 859                if existing_item_is_active && destination_index.is_none() {
 860                    insertion_index = existing_item_index;
 861                } else {
 862                    self.items.remove(existing_item_index);
 863                    if existing_item_index < self.active_item_index {
 864                        self.active_item_index -= 1;
 865                    }
 866                    insertion_index = insertion_index.min(self.items.len());
 867
 868                    self.items.insert(insertion_index, item.clone());
 869
 870                    if existing_item_is_active {
 871                        self.active_item_index = insertion_index;
 872                    } else if insertion_index <= self.active_item_index {
 873                        self.active_item_index += 1;
 874                    }
 875                }
 876
 877                cx.notify();
 878            }
 879
 880            self.activate_item(insertion_index, activate_pane, focus_item, cx);
 881        } else {
 882            self.items.insert(insertion_index, item.clone());
 883
 884            if insertion_index <= self.active_item_index
 885                && self.preview_item_idx() != Some(self.active_item_index)
 886            {
 887                self.active_item_index += 1;
 888            }
 889
 890            self.activate_item(insertion_index, activate_pane, focus_item, cx);
 891            cx.notify();
 892        }
 893
 894        cx.emit(Event::AddItem { item });
 895    }
 896
 897    pub fn items_len(&self) -> usize {
 898        self.items.len()
 899    }
 900
 901    pub fn items(&self) -> impl DoubleEndedIterator<Item = &Box<dyn ItemHandle>> {
 902        self.items.iter()
 903    }
 904
 905    pub fn items_of_type<T: Render>(&self) -> impl '_ + Iterator<Item = View<T>> {
 906        self.items
 907            .iter()
 908            .filter_map(|item| item.to_any().downcast().ok())
 909    }
 910
 911    pub fn active_item(&self) -> Option<Box<dyn ItemHandle>> {
 912        self.items.get(self.active_item_index).cloned()
 913    }
 914
 915    pub fn pixel_position_of_cursor(&self, cx: &AppContext) -> Option<Point<Pixels>> {
 916        self.items
 917            .get(self.active_item_index)?
 918            .pixel_position_of_cursor(cx)
 919    }
 920
 921    pub fn item_for_entry(
 922        &self,
 923        entry_id: ProjectEntryId,
 924        cx: &AppContext,
 925    ) -> Option<Box<dyn ItemHandle>> {
 926        self.items.iter().find_map(|item| {
 927            if item.is_singleton(cx) && (item.project_entry_ids(cx).as_slice() == [entry_id]) {
 928                Some(item.boxed_clone())
 929            } else {
 930                None
 931            }
 932        })
 933    }
 934
 935    pub fn item_for_path(
 936        &self,
 937        project_path: ProjectPath,
 938        cx: &AppContext,
 939    ) -> Option<Box<dyn ItemHandle>> {
 940        self.items.iter().find_map(move |item| {
 941            if item.is_singleton(cx) && (item.project_path(cx).as_slice() == [project_path.clone()])
 942            {
 943                Some(item.boxed_clone())
 944            } else {
 945                None
 946            }
 947        })
 948    }
 949
 950    pub fn index_for_item(&self, item: &dyn ItemHandle) -> Option<usize> {
 951        self.items
 952            .iter()
 953            .position(|i| i.item_id() == item.item_id())
 954    }
 955
 956    pub fn item_for_index(&self, ix: usize) -> Option<&dyn ItemHandle> {
 957        self.items.get(ix).map(|i| i.as_ref())
 958    }
 959
 960    pub fn toggle_zoom(&mut self, _: &ToggleZoom, cx: &mut ViewContext<Self>) {
 961        if self.zoomed {
 962            cx.emit(Event::ZoomOut);
 963        } else if !self.items.is_empty() {
 964            if !self.focus_handle.contains_focused(cx) {
 965                cx.focus_self();
 966            }
 967            cx.emit(Event::ZoomIn);
 968        }
 969    }
 970
 971    pub fn activate_item(
 972        &mut self,
 973        index: usize,
 974        activate_pane: bool,
 975        focus_item: bool,
 976        cx: &mut ViewContext<Self>,
 977    ) {
 978        use NavigationMode::{GoingBack, GoingForward};
 979
 980        if index < self.items.len() {
 981            let prev_active_item_ix = mem::replace(&mut self.active_item_index, index);
 982            if prev_active_item_ix != self.active_item_index
 983                || matches!(self.nav_history.mode(), GoingBack | GoingForward)
 984            {
 985                if let Some(prev_item) = self.items.get(prev_active_item_ix) {
 986                    prev_item.deactivated(cx);
 987                }
 988            }
 989            cx.emit(Event::ActivateItem {
 990                local: activate_pane,
 991            });
 992
 993            if let Some(newly_active_item) = self.items.get(index) {
 994                self.activation_history
 995                    .retain(|entry| entry.entity_id != newly_active_item.item_id());
 996                self.activation_history.push(ActivationHistoryEntry {
 997                    entity_id: newly_active_item.item_id(),
 998                    timestamp: self
 999                        .next_activation_timestamp
1000                        .fetch_add(1, Ordering::SeqCst),
1001                });
1002            }
1003
1004            self.update_toolbar(cx);
1005            self.update_status_bar(cx);
1006
1007            if focus_item {
1008                self.focus_active_item(cx);
1009            }
1010
1011            self.tab_bar_scroll_handle.scroll_to_item(index);
1012            cx.notify();
1013        }
1014    }
1015
1016    pub fn activate_prev_item(&mut self, activate_pane: bool, cx: &mut ViewContext<Self>) {
1017        let mut index = self.active_item_index;
1018        if index > 0 {
1019            index -= 1;
1020        } else if !self.items.is_empty() {
1021            index = self.items.len() - 1;
1022        }
1023        self.activate_item(index, activate_pane, activate_pane, cx);
1024    }
1025
1026    pub fn activate_next_item(&mut self, activate_pane: bool, cx: &mut ViewContext<Self>) {
1027        let mut index = self.active_item_index;
1028        if index + 1 < self.items.len() {
1029            index += 1;
1030        } else {
1031            index = 0;
1032        }
1033        self.activate_item(index, activate_pane, activate_pane, cx);
1034    }
1035
1036    pub fn close_active_item(
1037        &mut self,
1038        action: &CloseActiveItem,
1039        cx: &mut ViewContext<Self>,
1040    ) -> Option<Task<Result<()>>> {
1041        if self.items.is_empty() {
1042            // Close the window when there's no active items to close, if configured
1043            if WorkspaceSettings::get_global(cx)
1044                .when_closing_with_no_tabs
1045                .should_close()
1046            {
1047                cx.dispatch_action(Box::new(CloseWindow));
1048            }
1049
1050            return None;
1051        }
1052        let active_item_id = self.items[self.active_item_index].item_id();
1053        Some(self.close_item_by_id(
1054            active_item_id,
1055            action.save_intent.unwrap_or(SaveIntent::Close),
1056            cx,
1057        ))
1058    }
1059
1060    pub fn close_item_by_id(
1061        &mut self,
1062        item_id_to_close: EntityId,
1063        save_intent: SaveIntent,
1064        cx: &mut ViewContext<Self>,
1065    ) -> Task<Result<()>> {
1066        self.close_items(cx, save_intent, move |view_id| view_id == item_id_to_close)
1067    }
1068
1069    pub fn close_inactive_items(
1070        &mut self,
1071        action: &CloseInactiveItems,
1072        cx: &mut ViewContext<Self>,
1073    ) -> Option<Task<Result<()>>> {
1074        if self.items.is_empty() {
1075            return None;
1076        }
1077
1078        let active_item_id = self.items[self.active_item_index].item_id();
1079        Some(self.close_items(
1080            cx,
1081            action.save_intent.unwrap_or(SaveIntent::Close),
1082            move |item_id| item_id != active_item_id,
1083        ))
1084    }
1085
1086    pub fn close_clean_items(
1087        &mut self,
1088        _: &CloseCleanItems,
1089        cx: &mut ViewContext<Self>,
1090    ) -> Option<Task<Result<()>>> {
1091        let item_ids: Vec<_> = self
1092            .items()
1093            .filter(|item| !item.is_dirty(cx))
1094            .map(|item| item.item_id())
1095            .collect();
1096        Some(self.close_items(cx, SaveIntent::Close, move |item_id| {
1097            item_ids.contains(&item_id)
1098        }))
1099    }
1100
1101    pub fn close_items_to_the_left(
1102        &mut self,
1103        _: &CloseItemsToTheLeft,
1104        cx: &mut ViewContext<Self>,
1105    ) -> Option<Task<Result<()>>> {
1106        if self.items.is_empty() {
1107            return None;
1108        }
1109        let active_item_id = self.items[self.active_item_index].item_id();
1110        Some(self.close_items_to_the_left_by_id(active_item_id, cx))
1111    }
1112
1113    pub fn close_items_to_the_left_by_id(
1114        &mut self,
1115        item_id: EntityId,
1116        cx: &mut ViewContext<Self>,
1117    ) -> Task<Result<()>> {
1118        let item_ids: Vec<_> = self
1119            .items()
1120            .take_while(|item| item.item_id() != item_id)
1121            .map(|item| item.item_id())
1122            .collect();
1123        self.close_items(cx, SaveIntent::Close, move |item_id| {
1124            item_ids.contains(&item_id)
1125        })
1126    }
1127
1128    pub fn close_items_to_the_right(
1129        &mut self,
1130        _: &CloseItemsToTheRight,
1131        cx: &mut ViewContext<Self>,
1132    ) -> Option<Task<Result<()>>> {
1133        if self.items.is_empty() {
1134            return None;
1135        }
1136        let active_item_id = self.items[self.active_item_index].item_id();
1137        Some(self.close_items_to_the_right_by_id(active_item_id, cx))
1138    }
1139
1140    pub fn close_items_to_the_right_by_id(
1141        &mut self,
1142        item_id: EntityId,
1143        cx: &mut ViewContext<Self>,
1144    ) -> Task<Result<()>> {
1145        let item_ids: Vec<_> = self
1146            .items()
1147            .rev()
1148            .take_while(|item| item.item_id() != item_id)
1149            .map(|item| item.item_id())
1150            .collect();
1151        self.close_items(cx, SaveIntent::Close, move |item_id| {
1152            item_ids.contains(&item_id)
1153        })
1154    }
1155
1156    pub fn close_all_items(
1157        &mut self,
1158        action: &CloseAllItems,
1159        cx: &mut ViewContext<Self>,
1160    ) -> Option<Task<Result<()>>> {
1161        if self.items.is_empty() {
1162            return None;
1163        }
1164
1165        Some(
1166            self.close_items(cx, action.save_intent.unwrap_or(SaveIntent::Close), |_| {
1167                true
1168            }),
1169        )
1170    }
1171
1172    pub(super) fn file_names_for_prompt(
1173        items: &mut dyn Iterator<Item = &Box<dyn ItemHandle>>,
1174        all_dirty_items: usize,
1175        cx: &AppContext,
1176    ) -> (String, String) {
1177        /// Quantity of item paths displayed in prompt prior to cutoff..
1178        const FILE_NAMES_CUTOFF_POINT: usize = 10;
1179        let mut file_names: Vec<_> = items
1180            .filter_map(|item| {
1181                item.project_path(cx).and_then(|project_path| {
1182                    project_path
1183                        .path
1184                        .file_name()
1185                        .and_then(|name| name.to_str().map(ToOwned::to_owned))
1186                })
1187            })
1188            .take(FILE_NAMES_CUTOFF_POINT)
1189            .collect();
1190        let should_display_followup_text =
1191            all_dirty_items > FILE_NAMES_CUTOFF_POINT || file_names.len() != all_dirty_items;
1192        if should_display_followup_text {
1193            let not_shown_files = all_dirty_items - file_names.len();
1194            if not_shown_files == 1 {
1195                file_names.push(".. 1 file not shown".into());
1196            } else {
1197                file_names.push(format!(".. {} files not shown", not_shown_files));
1198            }
1199        }
1200        (
1201            format!(
1202                "Do you want to save changes to the following {} files?",
1203                all_dirty_items
1204            ),
1205            file_names.join("\n"),
1206        )
1207    }
1208
1209    pub fn close_items(
1210        &mut self,
1211        cx: &mut ViewContext<Pane>,
1212        mut save_intent: SaveIntent,
1213        should_close: impl Fn(EntityId) -> bool,
1214    ) -> Task<Result<()>> {
1215        // Find the items to close.
1216        let mut items_to_close = Vec::new();
1217        let mut dirty_items = Vec::new();
1218        for item in &self.items {
1219            if should_close(item.item_id()) {
1220                items_to_close.push(item.boxed_clone());
1221                if item.is_dirty(cx) {
1222                    dirty_items.push(item.boxed_clone());
1223                }
1224            }
1225        }
1226
1227        let active_item_id = self.active_item().map(|item| item.item_id());
1228
1229        items_to_close.sort_by_key(|item| {
1230            // Put the currently active item at the end, because if the currently active item is not closed last
1231            // closing the currently active item will cause the focus to switch to another item
1232            // This will cause Zed to expand the content of the currently active item
1233            active_item_id.filter(|&id| id == item.item_id()).is_some()
1234              // If a buffer is open both in a singleton editor and in a multibuffer, make sure
1235              // to focus the singleton buffer when prompting to save that buffer, as opposed
1236              // to focusing the multibuffer, because this gives the user a more clear idea
1237              // of what content they would be saving.
1238              || !item.is_singleton(cx)
1239        });
1240
1241        let workspace = self.workspace.clone();
1242        cx.spawn(|pane, mut cx| async move {
1243            if save_intent == SaveIntent::Close && dirty_items.len() > 1 {
1244                let answer = pane.update(&mut cx, |_, cx| {
1245                    let (prompt, detail) =
1246                        Self::file_names_for_prompt(&mut dirty_items.iter(), dirty_items.len(), cx);
1247                    cx.prompt(
1248                        PromptLevel::Warning,
1249                        &prompt,
1250                        Some(&detail),
1251                        &["Save all", "Discard all", "Cancel"],
1252                    )
1253                })?;
1254                match answer.await {
1255                    Ok(0) => save_intent = SaveIntent::SaveAll,
1256                    Ok(1) => save_intent = SaveIntent::Skip,
1257                    _ => {}
1258                }
1259            }
1260            let mut saved_project_items_ids = HashSet::default();
1261            for item in items_to_close.clone() {
1262                // Find the item's current index and its set of project item models. Avoid
1263                // storing these in advance, in case they have changed since this task
1264                // was started.
1265                let (item_ix, mut project_item_ids) = pane.update(&mut cx, |pane, cx| {
1266                    (pane.index_for_item(&*item), item.project_item_model_ids(cx))
1267                })?;
1268                let item_ix = if let Some(ix) = item_ix {
1269                    ix
1270                } else {
1271                    continue;
1272                };
1273
1274                // Check if this view has any project items that are not open anywhere else
1275                // in the workspace, AND that the user has not already been prompted to save.
1276                // If there are any such project entries, prompt the user to save this item.
1277                let project = workspace.update(&mut cx, |workspace, cx| {
1278                    for item in workspace.items(cx) {
1279                        if !items_to_close
1280                            .iter()
1281                            .any(|item_to_close| item_to_close.item_id() == item.item_id())
1282                        {
1283                            let other_project_item_ids = item.project_item_model_ids(cx);
1284                            project_item_ids.retain(|id| !other_project_item_ids.contains(id));
1285                        }
1286                    }
1287                    workspace.project().clone()
1288                })?;
1289                let should_save = project_item_ids
1290                    .iter()
1291                    .any(|id| saved_project_items_ids.insert(*id));
1292
1293                if should_save
1294                    && !Self::save_item(
1295                        project.clone(),
1296                        &pane,
1297                        item_ix,
1298                        &*item,
1299                        save_intent,
1300                        &mut cx,
1301                    )
1302                    .await?
1303                {
1304                    break;
1305                }
1306
1307                // Remove the item from the pane.
1308                pane.update(&mut cx, |pane, cx| {
1309                    if let Some(item_ix) = pane
1310                        .items
1311                        .iter()
1312                        .position(|i| i.item_id() == item.item_id())
1313                    {
1314                        pane.remove_item(item_ix, false, true, cx);
1315                    }
1316                })
1317                .ok();
1318            }
1319
1320            pane.update(&mut cx, |_, cx| cx.notify()).ok();
1321            Ok(())
1322        })
1323    }
1324
1325    pub fn remove_item(
1326        &mut self,
1327        item_index: usize,
1328        activate_pane: bool,
1329        close_pane_if_empty: bool,
1330        cx: &mut ViewContext<Self>,
1331    ) {
1332        self._remove_item(item_index, activate_pane, close_pane_if_empty, None, cx)
1333    }
1334
1335    pub fn remove_item_and_focus_on_pane(
1336        &mut self,
1337        item_index: usize,
1338        activate_pane: bool,
1339        focus_on_pane_if_closed: View<Pane>,
1340        cx: &mut ViewContext<Self>,
1341    ) {
1342        self._remove_item(
1343            item_index,
1344            activate_pane,
1345            true,
1346            Some(focus_on_pane_if_closed),
1347            cx,
1348        )
1349    }
1350
1351    fn _remove_item(
1352        &mut self,
1353        item_index: usize,
1354        activate_pane: bool,
1355        close_pane_if_empty: bool,
1356        focus_on_pane_if_closed: Option<View<Pane>>,
1357        cx: &mut ViewContext<Self>,
1358    ) {
1359        self.activation_history
1360            .retain(|entry| entry.entity_id != self.items[item_index].item_id());
1361
1362        if item_index == self.active_item_index {
1363            let index_to_activate = self
1364                .activation_history
1365                .pop()
1366                .and_then(|last_activated_item| {
1367                    self.items.iter().enumerate().find_map(|(index, item)| {
1368                        (item.item_id() == last_activated_item.entity_id).then_some(index)
1369                    })
1370                })
1371                // We didn't have a valid activation history entry, so fallback
1372                // to activating the item to the left
1373                .unwrap_or_else(|| item_index.min(self.items.len()).saturating_sub(1));
1374
1375            let should_activate = activate_pane || self.has_focus(cx);
1376            if self.items.len() == 1 && should_activate {
1377                self.focus_handle.focus(cx);
1378            } else {
1379                self.activate_item(index_to_activate, should_activate, should_activate, cx);
1380            }
1381        }
1382
1383        cx.emit(Event::RemoveItem { idx: item_index });
1384
1385        let item = self.items.remove(item_index);
1386
1387        cx.emit(Event::RemovedItem {
1388            item_id: item.item_id(),
1389        });
1390        if self.items.is_empty() {
1391            item.deactivated(cx);
1392            if close_pane_if_empty {
1393                self.update_toolbar(cx);
1394                cx.emit(Event::Remove {
1395                    focus_on_pane: focus_on_pane_if_closed,
1396                });
1397            }
1398        }
1399
1400        if item_index < self.active_item_index {
1401            self.active_item_index -= 1;
1402        }
1403
1404        let mode = self.nav_history.mode();
1405        self.nav_history.set_mode(NavigationMode::ClosingItem);
1406        item.deactivated(cx);
1407        self.nav_history.set_mode(mode);
1408
1409        if self.is_active_preview_item(item.item_id()) {
1410            self.set_preview_item_id(None, cx);
1411        }
1412
1413        if let Some(path) = item.project_path(cx) {
1414            let abs_path = self
1415                .nav_history
1416                .0
1417                .lock()
1418                .paths_by_item
1419                .get(&item.item_id())
1420                .and_then(|(_, abs_path)| abs_path.clone());
1421
1422            self.nav_history
1423                .0
1424                .lock()
1425                .paths_by_item
1426                .insert(item.item_id(), (path, abs_path));
1427        } else {
1428            self.nav_history
1429                .0
1430                .lock()
1431                .paths_by_item
1432                .remove(&item.item_id());
1433        }
1434
1435        if self.items.is_empty() && close_pane_if_empty && self.zoomed {
1436            cx.emit(Event::ZoomOut);
1437        }
1438
1439        cx.notify();
1440    }
1441
1442    pub async fn save_item(
1443        project: Model<Project>,
1444        pane: &WeakView<Pane>,
1445        item_ix: usize,
1446        item: &dyn ItemHandle,
1447        save_intent: SaveIntent,
1448        cx: &mut AsyncWindowContext,
1449    ) -> Result<bool> {
1450        const CONFLICT_MESSAGE: &str =
1451                "This file has changed on disk since you started editing it. Do you want to overwrite it?";
1452
1453        if save_intent == SaveIntent::Skip {
1454            return Ok(true);
1455        }
1456
1457        let (mut has_conflict, mut is_dirty, mut can_save, can_save_as) = cx.update(|cx| {
1458            (
1459                item.has_conflict(cx),
1460                item.is_dirty(cx),
1461                item.can_save(cx),
1462                item.is_singleton(cx),
1463            )
1464        })?;
1465
1466        // when saving a single buffer, we ignore whether or not it's dirty.
1467        if save_intent == SaveIntent::Save || save_intent == SaveIntent::SaveWithoutFormat {
1468            is_dirty = true;
1469        }
1470
1471        if save_intent == SaveIntent::SaveAs {
1472            is_dirty = true;
1473            has_conflict = false;
1474            can_save = false;
1475        }
1476
1477        if save_intent == SaveIntent::Overwrite {
1478            has_conflict = false;
1479        }
1480
1481        let should_format = save_intent != SaveIntent::SaveWithoutFormat;
1482
1483        if has_conflict && can_save {
1484            let answer = pane.update(cx, |pane, cx| {
1485                pane.activate_item(item_ix, true, true, cx);
1486                cx.prompt(
1487                    PromptLevel::Warning,
1488                    CONFLICT_MESSAGE,
1489                    None,
1490                    &["Overwrite", "Discard", "Cancel"],
1491                )
1492            })?;
1493            match answer.await {
1494                Ok(0) => {
1495                    pane.update(cx, |_, cx| item.save(should_format, project, cx))?
1496                        .await?
1497                }
1498                Ok(1) => pane.update(cx, |_, cx| item.reload(project, cx))?.await?,
1499                _ => return Ok(false),
1500            }
1501        } else if is_dirty && (can_save || can_save_as) {
1502            if save_intent == SaveIntent::Close {
1503                let will_autosave = cx.update(|cx| {
1504                    matches!(
1505                        item.workspace_settings(cx).autosave,
1506                        AutosaveSetting::OnFocusChange | AutosaveSetting::OnWindowChange
1507                    ) && Self::can_autosave_item(item, cx)
1508                })?;
1509                if !will_autosave {
1510                    let item_id = item.item_id();
1511                    let answer_task = pane.update(cx, |pane, cx| {
1512                        if pane.save_modals_spawned.insert(item_id) {
1513                            pane.activate_item(item_ix, true, true, cx);
1514                            let prompt = dirty_message_for(item.project_path(cx));
1515                            Some(cx.prompt(
1516                                PromptLevel::Warning,
1517                                &prompt,
1518                                None,
1519                                &["Save", "Don't Save", "Cancel"],
1520                            ))
1521                        } else {
1522                            None
1523                        }
1524                    })?;
1525                    if let Some(answer_task) = answer_task {
1526                        let answer = answer_task.await;
1527                        pane.update(cx, |pane, _| {
1528                            if !pane.save_modals_spawned.remove(&item_id) {
1529                                debug_panic!(
1530                                    "save modal was not present in spawned modals after awaiting for its answer"
1531                                )
1532                            }
1533                        })?;
1534                        match answer {
1535                            Ok(0) => {}
1536                            Ok(1) => {
1537                                // Don't save this file
1538                                pane.update(cx, |_, cx| item.discarded(project, cx))
1539                                    .log_err();
1540                                return Ok(true);
1541                            }
1542                            _ => return Ok(false), // Cancel
1543                        }
1544                    } else {
1545                        return Ok(false);
1546                    }
1547                }
1548            }
1549
1550            if can_save {
1551                pane.update(cx, |_, cx| item.save(should_format, project, cx))?
1552                    .await?;
1553            } else if can_save_as {
1554                let abs_path = pane.update(cx, |pane, cx| {
1555                    pane.workspace
1556                        .update(cx, |workspace, cx| workspace.prompt_for_new_path(cx))
1557                })??;
1558                if let Some(abs_path) = abs_path.await.ok().flatten() {
1559                    pane.update(cx, |pane, cx| {
1560                        if let Some(item) = pane.item_for_path(abs_path.clone(), cx) {
1561                            if let Some(idx) = pane.index_for_item(&*item) {
1562                                pane.remove_item(idx, false, false, cx);
1563                            }
1564                        }
1565
1566                        item.save_as(project, abs_path, cx)
1567                    })?
1568                    .await?;
1569                } else {
1570                    return Ok(false);
1571                }
1572            }
1573        }
1574
1575        pane.update(cx, |_, cx| {
1576            cx.emit(Event::UserSavedItem {
1577                item: item.downgrade_item(),
1578                save_intent,
1579            });
1580            true
1581        })
1582    }
1583
1584    fn can_autosave_item(item: &dyn ItemHandle, cx: &AppContext) -> bool {
1585        let is_deleted = item.project_entry_ids(cx).is_empty();
1586        item.is_dirty(cx) && !item.has_conflict(cx) && item.can_save(cx) && !is_deleted
1587    }
1588
1589    pub fn autosave_item(
1590        item: &dyn ItemHandle,
1591        project: Model<Project>,
1592        cx: &mut WindowContext,
1593    ) -> Task<Result<()>> {
1594        let format =
1595            if let AutosaveSetting::AfterDelay { .. } = item.workspace_settings(cx).autosave {
1596                false
1597            } else {
1598                true
1599            };
1600        if Self::can_autosave_item(item, cx) {
1601            item.save(format, project, cx)
1602        } else {
1603            Task::ready(Ok(()))
1604        }
1605    }
1606
1607    pub fn focus(&mut self, cx: &mut ViewContext<Pane>) {
1608        cx.focus(&self.focus_handle);
1609    }
1610
1611    pub fn focus_active_item(&mut self, cx: &mut ViewContext<Self>) {
1612        if let Some(active_item) = self.active_item() {
1613            let focus_handle = active_item.focus_handle(cx);
1614            cx.focus(&focus_handle);
1615        }
1616    }
1617
1618    pub fn split(&mut self, direction: SplitDirection, cx: &mut ViewContext<Self>) {
1619        cx.emit(Event::Split(direction));
1620    }
1621
1622    pub fn toolbar(&self) -> &View<Toolbar> {
1623        &self.toolbar
1624    }
1625
1626    pub fn handle_deleted_project_item(
1627        &mut self,
1628        entry_id: ProjectEntryId,
1629        cx: &mut ViewContext<Pane>,
1630    ) -> Option<()> {
1631        let (item_index_to_delete, item_id) = self.items().enumerate().find_map(|(i, item)| {
1632            if item.is_singleton(cx) && item.project_entry_ids(cx).as_slice() == [entry_id] {
1633                Some((i, item.item_id()))
1634            } else {
1635                None
1636            }
1637        })?;
1638
1639        self.remove_item(item_index_to_delete, false, true, cx);
1640        self.nav_history.remove_item(item_id);
1641
1642        Some(())
1643    }
1644
1645    fn update_toolbar(&mut self, cx: &mut ViewContext<Self>) {
1646        let active_item = self
1647            .items
1648            .get(self.active_item_index)
1649            .map(|item| item.as_ref());
1650        self.toolbar.update(cx, |toolbar, cx| {
1651            toolbar.set_active_item(active_item, cx);
1652        });
1653    }
1654
1655    fn update_status_bar(&mut self, cx: &mut ViewContext<Self>) {
1656        let workspace = self.workspace.clone();
1657        let pane = cx.view().clone();
1658
1659        cx.window_context().defer(move |cx| {
1660            let Ok(status_bar) = workspace.update(cx, |workspace, _| workspace.status_bar.clone())
1661            else {
1662                return;
1663            };
1664
1665            status_bar.update(cx, move |status_bar, cx| {
1666                status_bar.set_active_pane(&pane, cx);
1667            });
1668        });
1669    }
1670
1671    fn entry_abs_path(&self, entry: ProjectEntryId, cx: &WindowContext) -> Option<PathBuf> {
1672        let worktree = self
1673            .workspace
1674            .upgrade()?
1675            .read(cx)
1676            .project()
1677            .read(cx)
1678            .worktree_for_entry(entry, cx)?
1679            .read(cx);
1680        let entry = worktree.entry_for_id(entry)?;
1681        let abs_path = worktree.absolutize(&entry.path).ok()?;
1682        if entry.is_symlink {
1683            abs_path.canonicalize().ok()
1684        } else {
1685            Some(abs_path)
1686        }
1687    }
1688
1689    fn copy_relative_path(&mut self, _: &CopyRelativePath, cx: &mut ViewContext<Self>) {
1690        if let Some(clipboard_text) = self
1691            .active_item()
1692            .as_ref()
1693            .and_then(|entry| entry.project_path(cx))
1694            .map(|p| p.path.to_string_lossy().to_string())
1695        {
1696            cx.write_to_clipboard(ClipboardItem::new_string(clipboard_text));
1697        }
1698    }
1699
1700    pub fn icon_color(selected: bool) -> Color {
1701        if selected {
1702            Color::Default
1703        } else {
1704            Color::Muted
1705        }
1706    }
1707
1708    pub fn git_aware_icon_color(
1709        git_status: Option<GitFileStatus>,
1710        ignored: bool,
1711        selected: bool,
1712    ) -> Color {
1713        if ignored {
1714            Color::Ignored
1715        } else {
1716            match git_status {
1717                Some(GitFileStatus::Added) => Color::Created,
1718                Some(GitFileStatus::Modified) => Color::Modified,
1719                Some(GitFileStatus::Conflict) => Color::Conflict,
1720                None => Self::icon_color(selected),
1721            }
1722        }
1723    }
1724
1725    fn render_tab(
1726        &self,
1727        ix: usize,
1728        item: &dyn ItemHandle,
1729        detail: usize,
1730        cx: &mut ViewContext<'_, Pane>,
1731    ) -> impl IntoElement {
1732        let project_path = item.project_path(cx);
1733
1734        let is_active = ix == self.active_item_index;
1735        let is_preview = self
1736            .preview_item_id
1737            .map(|id| id == item.item_id())
1738            .unwrap_or(false);
1739
1740        let label = item.tab_content(
1741            TabContentParams {
1742                detail: Some(detail),
1743                selected: is_active,
1744                preview: is_preview,
1745            },
1746            cx,
1747        );
1748
1749        let icon_color = if ItemSettings::get_global(cx).git_status {
1750            project_path
1751                .as_ref()
1752                .and_then(|path| self.project.read(cx).entry_for_path(&path, cx))
1753                .map(|entry| {
1754                    Self::git_aware_icon_color(entry.git_status, entry.is_ignored, is_active)
1755                })
1756                .unwrap_or_else(|| Self::icon_color(is_active))
1757        } else {
1758            Self::icon_color(is_active)
1759        };
1760
1761        let icon = item.tab_icon(cx);
1762        let close_side = &ItemSettings::get_global(cx).close_position;
1763        let indicator = render_item_indicator(item.boxed_clone(), cx);
1764        let item_id = item.item_id();
1765        let is_first_item = ix == 0;
1766        let is_last_item = ix == self.items.len() - 1;
1767        let position_relative_to_active_item = ix.cmp(&self.active_item_index);
1768
1769        let tab = Tab::new(ix)
1770            .position(if is_first_item {
1771                TabPosition::First
1772            } else if is_last_item {
1773                TabPosition::Last
1774            } else {
1775                TabPosition::Middle(position_relative_to_active_item)
1776            })
1777            .close_side(match close_side {
1778                ClosePosition::Left => ui::TabCloseSide::Start,
1779                ClosePosition::Right => ui::TabCloseSide::End,
1780            })
1781            .selected(is_active)
1782            .on_click(
1783                cx.listener(move |pane: &mut Self, _, cx| pane.activate_item(ix, true, true, cx)),
1784            )
1785            // TODO: This should be a click listener with the middle mouse button instead of a mouse down listener.
1786            .on_mouse_down(
1787                MouseButton::Middle,
1788                cx.listener(move |pane, _event, cx| {
1789                    pane.close_item_by_id(item_id, SaveIntent::Close, cx)
1790                        .detach_and_log_err(cx);
1791                }),
1792            )
1793            .on_mouse_down(
1794                MouseButton::Left,
1795                cx.listener(move |pane, event: &MouseDownEvent, cx| {
1796                    if let Some(id) = pane.preview_item_id {
1797                        if id == item_id && event.click_count > 1 {
1798                            pane.set_preview_item_id(None, cx);
1799                        }
1800                    }
1801                }),
1802            )
1803            .on_drag(
1804                DraggedTab {
1805                    item: item.boxed_clone(),
1806                    pane: cx.view().clone(),
1807                    detail,
1808                    is_active,
1809                    ix,
1810                },
1811                |tab, cx| cx.new_view(|_| tab.clone()),
1812            )
1813            .drag_over::<DraggedTab>(|tab, _, cx| {
1814                tab.bg(cx.theme().colors().drop_target_background)
1815            })
1816            .drag_over::<DraggedSelection>(|tab, _, cx| {
1817                tab.bg(cx.theme().colors().drop_target_background)
1818            })
1819            .when_some(self.can_drop_predicate.clone(), |this, p| {
1820                this.can_drop(move |a, cx| p(a, cx))
1821            })
1822            .on_drop(cx.listener(move |this, dragged_tab: &DraggedTab, cx| {
1823                this.drag_split_direction = None;
1824                this.handle_tab_drop(dragged_tab, ix, cx)
1825            }))
1826            .on_drop(cx.listener(move |this, selection: &DraggedSelection, cx| {
1827                this.drag_split_direction = None;
1828                this.handle_dragged_selection_drop(selection, cx)
1829            }))
1830            .on_drop(cx.listener(move |this, paths, cx| {
1831                this.drag_split_direction = None;
1832                this.handle_external_paths_drop(paths, cx)
1833            }))
1834            .when_some(item.tab_tooltip_text(cx), |tab, text| {
1835                tab.tooltip(move |cx| Tooltip::text(text.clone(), cx))
1836            })
1837            .start_slot::<Indicator>(indicator)
1838            .end_slot(
1839                IconButton::new("close tab", IconName::Close)
1840                    .shape(IconButtonShape::Square)
1841                    .icon_color(Color::Muted)
1842                    .size(ButtonSize::None)
1843                    .icon_size(IconSize::XSmall)
1844                    .on_click(cx.listener(move |pane, _, cx| {
1845                        pane.close_item_by_id(item_id, SaveIntent::Close, cx)
1846                            .detach_and_log_err(cx);
1847                    })),
1848            )
1849            .child(
1850                h_flex()
1851                    .gap_1()
1852                    .children(icon.map(|icon| icon.size(IconSize::Small).color(icon_color)))
1853                    .child(label),
1854            );
1855
1856        let single_entry_to_resolve = {
1857            let item_entries = self.items[ix].project_entry_ids(cx);
1858            if item_entries.len() == 1 {
1859                Some(item_entries[0])
1860            } else {
1861                None
1862            }
1863        };
1864
1865        let pane = cx.view().downgrade();
1866        right_click_menu(ix).trigger(tab).menu(move |cx| {
1867            let pane = pane.clone();
1868            ContextMenu::build(cx, move |mut menu, cx| {
1869                if let Some(pane) = pane.upgrade() {
1870                    menu = menu
1871                        .entry(
1872                            "Close",
1873                            Some(Box::new(CloseActiveItem { save_intent: None })),
1874                            cx.handler_for(&pane, move |pane, cx| {
1875                                pane.close_item_by_id(item_id, SaveIntent::Close, cx)
1876                                    .detach_and_log_err(cx);
1877                            }),
1878                        )
1879                        .entry(
1880                            "Close Others",
1881                            Some(Box::new(CloseInactiveItems { save_intent: None })),
1882                            cx.handler_for(&pane, move |pane, cx| {
1883                                pane.close_items(cx, SaveIntent::Close, |id| id != item_id)
1884                                    .detach_and_log_err(cx);
1885                            }),
1886                        )
1887                        .separator()
1888                        .entry(
1889                            "Close Left",
1890                            Some(Box::new(CloseItemsToTheLeft)),
1891                            cx.handler_for(&pane, move |pane, cx| {
1892                                pane.close_items_to_the_left_by_id(item_id, cx)
1893                                    .detach_and_log_err(cx);
1894                            }),
1895                        )
1896                        .entry(
1897                            "Close Right",
1898                            Some(Box::new(CloseItemsToTheRight)),
1899                            cx.handler_for(&pane, move |pane, cx| {
1900                                pane.close_items_to_the_right_by_id(item_id, cx)
1901                                    .detach_and_log_err(cx);
1902                            }),
1903                        )
1904                        .separator()
1905                        .entry(
1906                            "Close Clean",
1907                            Some(Box::new(CloseCleanItems)),
1908                            cx.handler_for(&pane, move |pane, cx| {
1909                                if let Some(task) = pane.close_clean_items(&CloseCleanItems, cx) {
1910                                    task.detach_and_log_err(cx)
1911                                }
1912                            }),
1913                        )
1914                        .entry(
1915                            "Close All",
1916                            Some(Box::new(CloseAllItems { save_intent: None })),
1917                            cx.handler_for(&pane, |pane, cx| {
1918                                if let Some(task) =
1919                                    pane.close_all_items(&CloseAllItems { save_intent: None }, cx)
1920                                {
1921                                    task.detach_and_log_err(cx)
1922                                }
1923                            }),
1924                        );
1925
1926                    if let Some(entry) = single_entry_to_resolve {
1927                        let entry_abs_path = pane.read(cx).entry_abs_path(entry, cx);
1928                        let parent_abs_path = entry_abs_path
1929                            .as_deref()
1930                            .and_then(|abs_path| Some(abs_path.parent()?.to_path_buf()));
1931
1932                        let entry_id = entry.to_proto();
1933                        menu = menu
1934                            .separator()
1935                            .when_some(entry_abs_path, |menu, abs_path| {
1936                                menu.entry(
1937                                    "Copy Path",
1938                                    Some(Box::new(CopyPath)),
1939                                    cx.handler_for(&pane, move |_, cx| {
1940                                        cx.write_to_clipboard(ClipboardItem::new_string(
1941                                            abs_path.to_string_lossy().to_string(),
1942                                        ));
1943                                    }),
1944                                )
1945                            })
1946                            .entry(
1947                                "Copy Relative Path",
1948                                Some(Box::new(CopyRelativePath)),
1949                                cx.handler_for(&pane, move |pane, cx| {
1950                                    pane.copy_relative_path(&CopyRelativePath, cx);
1951                                }),
1952                            )
1953                            .separator()
1954                            .entry(
1955                                "Reveal In Project Panel",
1956                                Some(Box::new(RevealInProjectPanel {
1957                                    entry_id: Some(entry_id),
1958                                })),
1959                                cx.handler_for(&pane, move |pane, cx| {
1960                                    pane.project.update(cx, |_, cx| {
1961                                        cx.emit(project::Event::RevealInProjectPanel(
1962                                            ProjectEntryId::from_proto(entry_id),
1963                                        ))
1964                                    });
1965                                }),
1966                            )
1967                            .when_some(parent_abs_path, |menu, parent_abs_path| {
1968                                menu.entry(
1969                                    "Open in Terminal",
1970                                    Some(Box::new(OpenInTerminal)),
1971                                    cx.handler_for(&pane, move |_, cx| {
1972                                        cx.dispatch_action(
1973                                            OpenTerminal {
1974                                                working_directory: parent_abs_path.clone(),
1975                                            }
1976                                            .boxed_clone(),
1977                                        );
1978                                    }),
1979                                )
1980                            });
1981                    }
1982                }
1983
1984                menu
1985            })
1986        })
1987    }
1988
1989    fn render_tab_bar(&mut self, cx: &mut ViewContext<'_, Pane>) -> impl IntoElement {
1990        let focus_handle = self.focus_handle.clone();
1991        let navigate_backward = IconButton::new("navigate_backward", IconName::ArrowLeft)
1992            .shape(IconButtonShape::Square)
1993            .icon_size(IconSize::Small)
1994            .on_click({
1995                let view = cx.view().clone();
1996                move |_, cx| view.update(cx, Self::navigate_backward)
1997            })
1998            .disabled(!self.can_navigate_backward())
1999            .tooltip({
2000                let focus_handle = focus_handle.clone();
2001                move |cx| Tooltip::for_action_in("Go Back", &GoBack, &focus_handle, cx)
2002            });
2003
2004        let navigate_forward = IconButton::new("navigate_forward", IconName::ArrowRight)
2005            .shape(IconButtonShape::Square)
2006            .icon_size(IconSize::Small)
2007            .on_click({
2008                let view = cx.view().clone();
2009                move |_, cx| view.update(cx, Self::navigate_forward)
2010            })
2011            .disabled(!self.can_navigate_forward())
2012            .tooltip({
2013                let focus_handle = focus_handle.clone();
2014                move |cx| Tooltip::for_action_in("Go Forward", &GoForward, &focus_handle, cx)
2015            });
2016
2017        TabBar::new("tab_bar")
2018            .track_scroll(self.tab_bar_scroll_handle.clone())
2019            .when(
2020                self.display_nav_history_buttons.unwrap_or_default(),
2021                |tab_bar| {
2022                    tab_bar
2023                        .start_child(navigate_backward)
2024                        .start_child(navigate_forward)
2025                },
2026            )
2027            .map(|tab_bar| {
2028                let render_tab_buttons = self.render_tab_bar_buttons.clone();
2029                let (left_children, right_children) = render_tab_buttons(self, cx);
2030
2031                tab_bar
2032                    .start_children(left_children)
2033                    .end_children(right_children)
2034            })
2035            .children(
2036                self.items
2037                    .iter()
2038                    .enumerate()
2039                    .zip(tab_details(&self.items, cx))
2040                    .map(|((ix, item), detail)| self.render_tab(ix, &**item, detail, cx)),
2041            )
2042            .child(
2043                div()
2044                    .id("tab_bar_drop_target")
2045                    .min_w_6()
2046                    // HACK: This empty child is currently necessary to force the drop target to appear
2047                    // despite us setting a min width above.
2048                    .child("")
2049                    .h_full()
2050                    .flex_grow()
2051                    .drag_over::<DraggedTab>(|bar, _, cx| {
2052                        bar.bg(cx.theme().colors().drop_target_background)
2053                    })
2054                    .drag_over::<DraggedSelection>(|bar, _, cx| {
2055                        bar.bg(cx.theme().colors().drop_target_background)
2056                    })
2057                    .on_drop(cx.listener(move |this, dragged_tab: &DraggedTab, cx| {
2058                        this.drag_split_direction = None;
2059                        this.handle_tab_drop(dragged_tab, this.items.len(), cx)
2060                    }))
2061                    .on_drop(cx.listener(move |this, selection: &DraggedSelection, cx| {
2062                        this.drag_split_direction = None;
2063                        this.handle_project_entry_drop(&selection.active_selection.entry_id, cx)
2064                    }))
2065                    .on_drop(cx.listener(move |this, paths, cx| {
2066                        this.drag_split_direction = None;
2067                        this.handle_external_paths_drop(paths, cx)
2068                    }))
2069                    .on_click(cx.listener(move |this, event: &ClickEvent, cx| {
2070                        if event.up.click_count == 2 {
2071                            cx.dispatch_action(this.double_click_dispatch_action.boxed_clone())
2072                        }
2073                    })),
2074            )
2075    }
2076
2077    pub fn render_menu_overlay(menu: &View<ContextMenu>) -> Div {
2078        div().absolute().bottom_0().right_0().size_0().child(
2079            deferred(
2080                anchored()
2081                    .anchor(AnchorCorner::TopRight)
2082                    .child(menu.clone()),
2083            )
2084            .with_priority(1),
2085        )
2086    }
2087
2088    pub fn set_zoomed(&mut self, zoomed: bool, cx: &mut ViewContext<Self>) {
2089        self.zoomed = zoomed;
2090        cx.notify();
2091    }
2092
2093    pub fn is_zoomed(&self) -> bool {
2094        self.zoomed
2095    }
2096
2097    fn handle_drag_move<T>(&mut self, event: &DragMoveEvent<T>, cx: &mut ViewContext<Self>) {
2098        if !self.can_split {
2099            return;
2100        }
2101
2102        let rect = event.bounds.size;
2103
2104        let size = event.bounds.size.width.min(event.bounds.size.height)
2105            * WorkspaceSettings::get_global(cx).drop_target_size;
2106
2107        let relative_cursor = Point::new(
2108            event.event.position.x - event.bounds.left(),
2109            event.event.position.y - event.bounds.top(),
2110        );
2111
2112        let direction = if relative_cursor.x < size
2113            || relative_cursor.x > rect.width - size
2114            || relative_cursor.y < size
2115            || relative_cursor.y > rect.height - size
2116        {
2117            [
2118                SplitDirection::Up,
2119                SplitDirection::Right,
2120                SplitDirection::Down,
2121                SplitDirection::Left,
2122            ]
2123            .iter()
2124            .min_by_key(|side| match side {
2125                SplitDirection::Up => relative_cursor.y,
2126                SplitDirection::Right => rect.width - relative_cursor.x,
2127                SplitDirection::Down => rect.height - relative_cursor.y,
2128                SplitDirection::Left => relative_cursor.x,
2129            })
2130            .cloned()
2131        } else {
2132            None
2133        };
2134
2135        if direction != self.drag_split_direction {
2136            self.drag_split_direction = direction;
2137        }
2138    }
2139
2140    fn handle_tab_drop(
2141        &mut self,
2142        dragged_tab: &DraggedTab,
2143        ix: usize,
2144        cx: &mut ViewContext<'_, Self>,
2145    ) {
2146        if let Some(custom_drop_handle) = self.custom_drop_handle.clone() {
2147            if let ControlFlow::Break(()) = custom_drop_handle(self, dragged_tab, cx) {
2148                return;
2149            }
2150        }
2151        let mut to_pane = cx.view().clone();
2152        let split_direction = self.drag_split_direction;
2153        let item_id = dragged_tab.item.item_id();
2154        if let Some(preview_item_id) = self.preview_item_id {
2155            if item_id == preview_item_id {
2156                self.set_preview_item_id(None, cx);
2157            }
2158        }
2159
2160        let from_pane = dragged_tab.pane.clone();
2161        self.workspace
2162            .update(cx, |_, cx| {
2163                cx.defer(move |workspace, cx| {
2164                    if let Some(split_direction) = split_direction {
2165                        to_pane = workspace.split_pane(to_pane, split_direction, cx);
2166                    }
2167                    workspace.move_item(from_pane, to_pane, item_id, ix, cx);
2168                });
2169            })
2170            .log_err();
2171    }
2172
2173    fn handle_dragged_selection_drop(
2174        &mut self,
2175        dragged_selection: &DraggedSelection,
2176        cx: &mut ViewContext<'_, Self>,
2177    ) {
2178        if let Some(custom_drop_handle) = self.custom_drop_handle.clone() {
2179            if let ControlFlow::Break(()) = custom_drop_handle(self, dragged_selection, cx) {
2180                return;
2181            }
2182        }
2183        self.handle_project_entry_drop(&dragged_selection.active_selection.entry_id, cx);
2184    }
2185
2186    fn handle_project_entry_drop(
2187        &mut self,
2188        project_entry_id: &ProjectEntryId,
2189        cx: &mut ViewContext<'_, Self>,
2190    ) {
2191        if let Some(custom_drop_handle) = self.custom_drop_handle.clone() {
2192            if let ControlFlow::Break(()) = custom_drop_handle(self, project_entry_id, cx) {
2193                return;
2194            }
2195        }
2196        let mut to_pane = cx.view().clone();
2197        let split_direction = self.drag_split_direction;
2198        let project_entry_id = *project_entry_id;
2199        self.workspace
2200            .update(cx, |_, cx| {
2201                cx.defer(move |workspace, cx| {
2202                    if let Some(path) = workspace
2203                        .project()
2204                        .read(cx)
2205                        .path_for_entry(project_entry_id, cx)
2206                    {
2207                        let load_path_task = workspace.load_path(path, cx);
2208                        cx.spawn(|workspace, mut cx| async move {
2209                            if let Some((project_entry_id, build_item)) =
2210                                load_path_task.await.notify_async_err(&mut cx)
2211                            {
2212                                workspace
2213                                    .update(&mut cx, |workspace, cx| {
2214                                        if let Some(split_direction) = split_direction {
2215                                            to_pane =
2216                                                workspace.split_pane(to_pane, split_direction, cx);
2217                                        }
2218                                        to_pane.update(cx, |pane, cx| {
2219                                            pane.open_item(
2220                                                project_entry_id,
2221                                                true,
2222                                                false,
2223                                                cx,
2224                                                build_item,
2225                                            )
2226                                        })
2227                                    })
2228                                    .log_err();
2229                            }
2230                        })
2231                        .detach();
2232                    };
2233                });
2234            })
2235            .log_err();
2236    }
2237
2238    fn handle_external_paths_drop(
2239        &mut self,
2240        paths: &ExternalPaths,
2241        cx: &mut ViewContext<'_, Self>,
2242    ) {
2243        if let Some(custom_drop_handle) = self.custom_drop_handle.clone() {
2244            if let ControlFlow::Break(()) = custom_drop_handle(self, paths, cx) {
2245                return;
2246            }
2247        }
2248        let mut to_pane = cx.view().clone();
2249        let mut split_direction = self.drag_split_direction;
2250        let paths = paths.paths().to_vec();
2251        let is_remote = self
2252            .workspace
2253            .update(cx, |workspace, cx| {
2254                if workspace.project().read(cx).is_via_collab() {
2255                    workspace.show_error(
2256                        &anyhow::anyhow!("Cannot drop files on a remote project"),
2257                        cx,
2258                    );
2259                    true
2260                } else {
2261                    false
2262                }
2263            })
2264            .unwrap_or(true);
2265        if is_remote {
2266            return;
2267        }
2268
2269        self.workspace
2270            .update(cx, |workspace, cx| {
2271                let fs = Arc::clone(workspace.project().read(cx).fs());
2272                cx.spawn(|workspace, mut cx| async move {
2273                    let mut is_file_checks = FuturesUnordered::new();
2274                    for path in &paths {
2275                        is_file_checks.push(fs.is_file(path))
2276                    }
2277                    let mut has_files_to_open = false;
2278                    while let Some(is_file) = is_file_checks.next().await {
2279                        if is_file {
2280                            has_files_to_open = true;
2281                            break;
2282                        }
2283                    }
2284                    drop(is_file_checks);
2285                    if !has_files_to_open {
2286                        split_direction = None;
2287                    }
2288
2289                    if let Some(open_task) = workspace
2290                        .update(&mut cx, |workspace, cx| {
2291                            if let Some(split_direction) = split_direction {
2292                                to_pane = workspace.split_pane(to_pane, split_direction, cx);
2293                            }
2294                            workspace.open_paths(
2295                                paths,
2296                                OpenVisible::OnlyDirectories,
2297                                Some(to_pane.downgrade()),
2298                                cx,
2299                            )
2300                        })
2301                        .ok()
2302                    {
2303                        let opened_items: Vec<_> = open_task.await;
2304                        _ = workspace.update(&mut cx, |workspace, cx| {
2305                            for item in opened_items.into_iter().flatten() {
2306                                if let Err(e) = item {
2307                                    workspace.show_error(&e, cx);
2308                                }
2309                            }
2310                        });
2311                    }
2312                })
2313                .detach();
2314            })
2315            .log_err();
2316    }
2317
2318    pub fn display_nav_history_buttons(&mut self, display: Option<bool>) {
2319        self.display_nav_history_buttons = display;
2320    }
2321}
2322
2323impl FocusableView for Pane {
2324    fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
2325        self.focus_handle.clone()
2326    }
2327}
2328
2329impl Render for Pane {
2330    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
2331        let mut key_context = KeyContext::new_with_defaults();
2332        key_context.add("Pane");
2333        if self.active_item().is_none() {
2334            key_context.add("EmptyPane");
2335        }
2336
2337        let should_display_tab_bar = self.should_display_tab_bar.clone();
2338        let display_tab_bar = should_display_tab_bar(cx);
2339
2340        v_flex()
2341            .key_context(key_context)
2342            .track_focus(&self.focus_handle)
2343            .size_full()
2344            .flex_none()
2345            .overflow_hidden()
2346            .on_action(cx.listener(|pane, _: &AlternateFile, cx| {
2347                pane.alternate_file(cx);
2348            }))
2349            .on_action(cx.listener(|pane, _: &SplitLeft, cx| pane.split(SplitDirection::Left, cx)))
2350            .on_action(cx.listener(|pane, _: &SplitUp, cx| pane.split(SplitDirection::Up, cx)))
2351            .on_action(cx.listener(|pane, _: &SplitHorizontal, cx| {
2352                pane.split(SplitDirection::horizontal(cx), cx)
2353            }))
2354            .on_action(cx.listener(|pane, _: &SplitVertical, cx| {
2355                pane.split(SplitDirection::vertical(cx), cx)
2356            }))
2357            .on_action(
2358                cx.listener(|pane, _: &SplitRight, cx| pane.split(SplitDirection::Right, cx)),
2359            )
2360            .on_action(cx.listener(|pane, _: &SplitDown, cx| pane.split(SplitDirection::Down, cx)))
2361            .on_action(cx.listener(|pane, _: &GoBack, cx| pane.navigate_backward(cx)))
2362            .on_action(cx.listener(|pane, _: &GoForward, cx| pane.navigate_forward(cx)))
2363            .on_action(cx.listener(|pane, _: &JoinIntoNext, cx| pane.join_into_next(cx)))
2364            .on_action(cx.listener(Pane::toggle_zoom))
2365            .on_action(cx.listener(|pane: &mut Pane, action: &ActivateItem, cx| {
2366                pane.activate_item(action.0, true, true, cx);
2367            }))
2368            .on_action(cx.listener(|pane: &mut Pane, _: &ActivateLastItem, cx| {
2369                pane.activate_item(pane.items.len() - 1, true, true, cx);
2370            }))
2371            .on_action(cx.listener(|pane: &mut Pane, _: &ActivatePrevItem, cx| {
2372                pane.activate_prev_item(true, cx);
2373            }))
2374            .on_action(cx.listener(|pane: &mut Pane, _: &ActivateNextItem, cx| {
2375                pane.activate_next_item(true, cx);
2376            }))
2377            .when(PreviewTabsSettings::get_global(cx).enabled, |this| {
2378                this.on_action(cx.listener(|pane: &mut Pane, _: &TogglePreviewTab, cx| {
2379                    if let Some(active_item_id) = pane.active_item().map(|i| i.item_id()) {
2380                        if pane.is_active_preview_item(active_item_id) {
2381                            pane.set_preview_item_id(None, cx);
2382                        } else {
2383                            pane.set_preview_item_id(Some(active_item_id), cx);
2384                        }
2385                    }
2386                }))
2387            })
2388            .on_action(
2389                cx.listener(|pane: &mut Self, action: &CloseActiveItem, cx| {
2390                    if let Some(task) = pane.close_active_item(action, cx) {
2391                        task.detach_and_log_err(cx)
2392                    }
2393                }),
2394            )
2395            .on_action(
2396                cx.listener(|pane: &mut Self, action: &CloseInactiveItems, cx| {
2397                    if let Some(task) = pane.close_inactive_items(action, cx) {
2398                        task.detach_and_log_err(cx)
2399                    }
2400                }),
2401            )
2402            .on_action(
2403                cx.listener(|pane: &mut Self, action: &CloseCleanItems, cx| {
2404                    if let Some(task) = pane.close_clean_items(action, cx) {
2405                        task.detach_and_log_err(cx)
2406                    }
2407                }),
2408            )
2409            .on_action(
2410                cx.listener(|pane: &mut Self, action: &CloseItemsToTheLeft, cx| {
2411                    if let Some(task) = pane.close_items_to_the_left(action, cx) {
2412                        task.detach_and_log_err(cx)
2413                    }
2414                }),
2415            )
2416            .on_action(
2417                cx.listener(|pane: &mut Self, action: &CloseItemsToTheRight, cx| {
2418                    if let Some(task) = pane.close_items_to_the_right(action, cx) {
2419                        task.detach_and_log_err(cx)
2420                    }
2421                }),
2422            )
2423            .on_action(cx.listener(|pane: &mut Self, action: &CloseAllItems, cx| {
2424                if let Some(task) = pane.close_all_items(action, cx) {
2425                    task.detach_and_log_err(cx)
2426                }
2427            }))
2428            .on_action(
2429                cx.listener(|pane: &mut Self, action: &CloseActiveItem, cx| {
2430                    if let Some(task) = pane.close_active_item(action, cx) {
2431                        task.detach_and_log_err(cx)
2432                    }
2433                }),
2434            )
2435            .on_action(
2436                cx.listener(|pane: &mut Self, action: &RevealInProjectPanel, cx| {
2437                    let entry_id = action
2438                        .entry_id
2439                        .map(ProjectEntryId::from_proto)
2440                        .or_else(|| pane.active_item()?.project_entry_ids(cx).first().copied());
2441                    if let Some(entry_id) = entry_id {
2442                        pane.project.update(cx, |_, cx| {
2443                            cx.emit(project::Event::RevealInProjectPanel(entry_id))
2444                        });
2445                    }
2446                }),
2447            )
2448            .when(self.active_item().is_some() && display_tab_bar, |pane| {
2449                pane.child(self.render_tab_bar(cx))
2450            })
2451            .child({
2452                let has_worktrees = self.project.read(cx).worktrees(cx).next().is_some();
2453                // main content
2454                div()
2455                    .flex_1()
2456                    .relative()
2457                    .group("")
2458                    .on_drag_move::<DraggedTab>(cx.listener(Self::handle_drag_move))
2459                    .on_drag_move::<DraggedSelection>(cx.listener(Self::handle_drag_move))
2460                    .on_drag_move::<ExternalPaths>(cx.listener(Self::handle_drag_move))
2461                    .map(|div| {
2462                        if let Some(item) = self.active_item() {
2463                            div.v_flex()
2464                                .child(self.toolbar.clone())
2465                                .child(item.to_any())
2466                        } else {
2467                            let placeholder = div.h_flex().size_full().justify_center();
2468                            if has_worktrees {
2469                                placeholder
2470                            } else {
2471                                placeholder.child(
2472                                    Label::new("Open a file or project to get started.")
2473                                        .color(Color::Muted),
2474                                )
2475                            }
2476                        }
2477                    })
2478                    .child(
2479                        // drag target
2480                        div()
2481                            .invisible()
2482                            .absolute()
2483                            .bg(cx.theme().colors().drop_target_background)
2484                            .group_drag_over::<DraggedTab>("", |style| style.visible())
2485                            .group_drag_over::<DraggedSelection>("", |style| style.visible())
2486                            .group_drag_over::<ExternalPaths>("", |style| style.visible())
2487                            .when_some(self.can_drop_predicate.clone(), |this, p| {
2488                                this.can_drop(move |a, cx| p(a, cx))
2489                            })
2490                            .on_drop(cx.listener(move |this, dragged_tab, cx| {
2491                                this.handle_tab_drop(dragged_tab, this.active_item_index(), cx)
2492                            }))
2493                            .on_drop(cx.listener(move |this, selection: &DraggedSelection, cx| {
2494                                this.handle_dragged_selection_drop(selection, cx)
2495                            }))
2496                            .on_drop(cx.listener(move |this, paths, cx| {
2497                                this.handle_external_paths_drop(paths, cx)
2498                            }))
2499                            .map(|div| {
2500                                let size = DefiniteLength::Fraction(0.5);
2501                                match self.drag_split_direction {
2502                                    None => div.top_0().right_0().bottom_0().left_0(),
2503                                    Some(SplitDirection::Up) => {
2504                                        div.top_0().left_0().right_0().h(size)
2505                                    }
2506                                    Some(SplitDirection::Down) => {
2507                                        div.left_0().bottom_0().right_0().h(size)
2508                                    }
2509                                    Some(SplitDirection::Left) => {
2510                                        div.top_0().left_0().bottom_0().w(size)
2511                                    }
2512                                    Some(SplitDirection::Right) => {
2513                                        div.top_0().bottom_0().right_0().w(size)
2514                                    }
2515                                }
2516                            }),
2517                    )
2518            })
2519            .on_mouse_down(
2520                MouseButton::Navigate(NavigationDirection::Back),
2521                cx.listener(|pane, _, cx| {
2522                    if let Some(workspace) = pane.workspace.upgrade() {
2523                        let pane = cx.view().downgrade();
2524                        cx.window_context().defer(move |cx| {
2525                            workspace.update(cx, |workspace, cx| {
2526                                workspace.go_back(pane, cx).detach_and_log_err(cx)
2527                            })
2528                        })
2529                    }
2530                }),
2531            )
2532            .on_mouse_down(
2533                MouseButton::Navigate(NavigationDirection::Forward),
2534                cx.listener(|pane, _, cx| {
2535                    if let Some(workspace) = pane.workspace.upgrade() {
2536                        let pane = cx.view().downgrade();
2537                        cx.window_context().defer(move |cx| {
2538                            workspace.update(cx, |workspace, cx| {
2539                                workspace.go_forward(pane, cx).detach_and_log_err(cx)
2540                            })
2541                        })
2542                    }
2543                }),
2544            )
2545    }
2546}
2547
2548impl ItemNavHistory {
2549    pub fn push<D: 'static + Send + Any>(&mut self, data: Option<D>, cx: &mut WindowContext) {
2550        self.history
2551            .push(data, self.item.clone(), self.is_preview, cx);
2552    }
2553
2554    pub fn pop_backward(&mut self, cx: &mut WindowContext) -> Option<NavigationEntry> {
2555        self.history.pop(NavigationMode::GoingBack, cx)
2556    }
2557
2558    pub fn pop_forward(&mut self, cx: &mut WindowContext) -> Option<NavigationEntry> {
2559        self.history.pop(NavigationMode::GoingForward, cx)
2560    }
2561}
2562
2563impl NavHistory {
2564    pub fn for_each_entry(
2565        &self,
2566        cx: &AppContext,
2567        mut f: impl FnMut(&NavigationEntry, (ProjectPath, Option<PathBuf>)),
2568    ) {
2569        let borrowed_history = self.0.lock();
2570        borrowed_history
2571            .forward_stack
2572            .iter()
2573            .chain(borrowed_history.backward_stack.iter())
2574            .chain(borrowed_history.closed_stack.iter())
2575            .for_each(|entry| {
2576                if let Some(project_and_abs_path) =
2577                    borrowed_history.paths_by_item.get(&entry.item.id())
2578                {
2579                    f(entry, project_and_abs_path.clone());
2580                } else if let Some(item) = entry.item.upgrade() {
2581                    if let Some(path) = item.project_path(cx) {
2582                        f(entry, (path, None));
2583                    }
2584                }
2585            })
2586    }
2587
2588    pub fn set_mode(&mut self, mode: NavigationMode) {
2589        self.0.lock().mode = mode;
2590    }
2591
2592    pub fn mode(&self) -> NavigationMode {
2593        self.0.lock().mode
2594    }
2595
2596    pub fn disable(&mut self) {
2597        self.0.lock().mode = NavigationMode::Disabled;
2598    }
2599
2600    pub fn enable(&mut self) {
2601        self.0.lock().mode = NavigationMode::Normal;
2602    }
2603
2604    pub fn pop(&mut self, mode: NavigationMode, cx: &mut WindowContext) -> Option<NavigationEntry> {
2605        let mut state = self.0.lock();
2606        let entry = match mode {
2607            NavigationMode::Normal | NavigationMode::Disabled | NavigationMode::ClosingItem => {
2608                return None
2609            }
2610            NavigationMode::GoingBack => &mut state.backward_stack,
2611            NavigationMode::GoingForward => &mut state.forward_stack,
2612            NavigationMode::ReopeningClosedItem => &mut state.closed_stack,
2613        }
2614        .pop_back();
2615        if entry.is_some() {
2616            state.did_update(cx);
2617        }
2618        entry
2619    }
2620
2621    pub fn push<D: 'static + Send + Any>(
2622        &mut self,
2623        data: Option<D>,
2624        item: Arc<dyn WeakItemHandle>,
2625        is_preview: bool,
2626        cx: &mut WindowContext,
2627    ) {
2628        let state = &mut *self.0.lock();
2629        match state.mode {
2630            NavigationMode::Disabled => {}
2631            NavigationMode::Normal | NavigationMode::ReopeningClosedItem => {
2632                if state.backward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
2633                    state.backward_stack.pop_front();
2634                }
2635                state.backward_stack.push_back(NavigationEntry {
2636                    item,
2637                    data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
2638                    timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
2639                    is_preview,
2640                });
2641                state.forward_stack.clear();
2642            }
2643            NavigationMode::GoingBack => {
2644                if state.forward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
2645                    state.forward_stack.pop_front();
2646                }
2647                state.forward_stack.push_back(NavigationEntry {
2648                    item,
2649                    data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
2650                    timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
2651                    is_preview,
2652                });
2653            }
2654            NavigationMode::GoingForward => {
2655                if state.backward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
2656                    state.backward_stack.pop_front();
2657                }
2658                state.backward_stack.push_back(NavigationEntry {
2659                    item,
2660                    data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
2661                    timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
2662                    is_preview,
2663                });
2664            }
2665            NavigationMode::ClosingItem => {
2666                if state.closed_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
2667                    state.closed_stack.pop_front();
2668                }
2669                state.closed_stack.push_back(NavigationEntry {
2670                    item,
2671                    data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
2672                    timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
2673                    is_preview,
2674                });
2675            }
2676        }
2677        state.did_update(cx);
2678    }
2679
2680    pub fn remove_item(&mut self, item_id: EntityId) {
2681        let mut state = self.0.lock();
2682        state.paths_by_item.remove(&item_id);
2683        state
2684            .backward_stack
2685            .retain(|entry| entry.item.id() != item_id);
2686        state
2687            .forward_stack
2688            .retain(|entry| entry.item.id() != item_id);
2689        state
2690            .closed_stack
2691            .retain(|entry| entry.item.id() != item_id);
2692    }
2693
2694    pub fn path_for_item(&self, item_id: EntityId) -> Option<(ProjectPath, Option<PathBuf>)> {
2695        self.0.lock().paths_by_item.get(&item_id).cloned()
2696    }
2697}
2698
2699impl NavHistoryState {
2700    pub fn did_update(&self, cx: &mut WindowContext) {
2701        if let Some(pane) = self.pane.upgrade() {
2702            cx.defer(move |cx| {
2703                pane.update(cx, |pane, cx| pane.history_updated(cx));
2704            });
2705        }
2706    }
2707}
2708
2709fn dirty_message_for(buffer_path: Option<ProjectPath>) -> String {
2710    let path = buffer_path
2711        .as_ref()
2712        .and_then(|p| {
2713            p.path
2714                .to_str()
2715                .and_then(|s| if s == "" { None } else { Some(s) })
2716        })
2717        .unwrap_or("This buffer");
2718    let path = truncate_and_remove_front(path, 80);
2719    format!("{path} contains unsaved edits. Do you want to save it?")
2720}
2721
2722pub fn tab_details(items: &Vec<Box<dyn ItemHandle>>, cx: &AppContext) -> Vec<usize> {
2723    let mut tab_details = items.iter().map(|_| 0).collect::<Vec<_>>();
2724    let mut tab_descriptions = HashMap::default();
2725    let mut done = false;
2726    while !done {
2727        done = true;
2728
2729        // Store item indices by their tab description.
2730        for (ix, (item, detail)) in items.iter().zip(&tab_details).enumerate() {
2731            if let Some(description) = item.tab_description(*detail, cx) {
2732                if *detail == 0
2733                    || Some(&description) != item.tab_description(detail - 1, cx).as_ref()
2734                {
2735                    tab_descriptions
2736                        .entry(description)
2737                        .or_insert(Vec::new())
2738                        .push(ix);
2739                }
2740            }
2741        }
2742
2743        // If two or more items have the same tab description, increase their level
2744        // of detail and try again.
2745        for (_, item_ixs) in tab_descriptions.drain() {
2746            if item_ixs.len() > 1 {
2747                done = false;
2748                for ix in item_ixs {
2749                    tab_details[ix] += 1;
2750                }
2751            }
2752        }
2753    }
2754
2755    tab_details
2756}
2757
2758pub fn render_item_indicator(item: Box<dyn ItemHandle>, cx: &WindowContext) -> Option<Indicator> {
2759    maybe!({
2760        let indicator_color = match (item.has_conflict(cx), item.is_dirty(cx)) {
2761            (true, _) => Color::Warning,
2762            (_, true) => Color::Accent,
2763            (false, false) => return None,
2764        };
2765
2766        Some(Indicator::dot().color(indicator_color))
2767    })
2768}
2769
2770#[cfg(test)]
2771mod tests {
2772    use super::*;
2773    use crate::item::test::{TestItem, TestProjectItem};
2774    use gpui::{TestAppContext, VisualTestContext};
2775    use project::FakeFs;
2776    use settings::SettingsStore;
2777    use theme::LoadThemes;
2778
2779    #[gpui::test]
2780    async fn test_remove_active_empty(cx: &mut TestAppContext) {
2781        init_test(cx);
2782        let fs = FakeFs::new(cx.executor());
2783
2784        let project = Project::test(fs, None, cx).await;
2785        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
2786        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
2787
2788        pane.update(cx, |pane, cx| {
2789            assert!(pane
2790                .close_active_item(&CloseActiveItem { save_intent: None }, cx)
2791                .is_none())
2792        });
2793    }
2794
2795    #[gpui::test]
2796    async fn test_add_item_with_new_item(cx: &mut TestAppContext) {
2797        init_test(cx);
2798        let fs = FakeFs::new(cx.executor());
2799
2800        let project = Project::test(fs, None, cx).await;
2801        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
2802        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
2803
2804        // 1. Add with a destination index
2805        //   a. Add before the active item
2806        set_labeled_items(&pane, ["A", "B*", "C"], cx);
2807        pane.update(cx, |pane, cx| {
2808            pane.add_item(
2809                Box::new(cx.new_view(|cx| TestItem::new(cx).with_label("D"))),
2810                false,
2811                false,
2812                Some(0),
2813                cx,
2814            );
2815        });
2816        assert_item_labels(&pane, ["D*", "A", "B", "C"], cx);
2817
2818        //   b. Add after the active item
2819        set_labeled_items(&pane, ["A", "B*", "C"], cx);
2820        pane.update(cx, |pane, cx| {
2821            pane.add_item(
2822                Box::new(cx.new_view(|cx| TestItem::new(cx).with_label("D"))),
2823                false,
2824                false,
2825                Some(2),
2826                cx,
2827            );
2828        });
2829        assert_item_labels(&pane, ["A", "B", "D*", "C"], cx);
2830
2831        //   c. Add at the end of the item list (including off the length)
2832        set_labeled_items(&pane, ["A", "B*", "C"], cx);
2833        pane.update(cx, |pane, cx| {
2834            pane.add_item(
2835                Box::new(cx.new_view(|cx| TestItem::new(cx).with_label("D"))),
2836                false,
2837                false,
2838                Some(5),
2839                cx,
2840            );
2841        });
2842        assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
2843
2844        // 2. Add without a destination index
2845        //   a. Add with active item at the start of the item list
2846        set_labeled_items(&pane, ["A*", "B", "C"], cx);
2847        pane.update(cx, |pane, cx| {
2848            pane.add_item(
2849                Box::new(cx.new_view(|cx| TestItem::new(cx).with_label("D"))),
2850                false,
2851                false,
2852                None,
2853                cx,
2854            );
2855        });
2856        set_labeled_items(&pane, ["A", "D*", "B", "C"], cx);
2857
2858        //   b. Add with active item at the end of the item list
2859        set_labeled_items(&pane, ["A", "B", "C*"], cx);
2860        pane.update(cx, |pane, cx| {
2861            pane.add_item(
2862                Box::new(cx.new_view(|cx| TestItem::new(cx).with_label("D"))),
2863                false,
2864                false,
2865                None,
2866                cx,
2867            );
2868        });
2869        assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
2870    }
2871
2872    #[gpui::test]
2873    async fn test_add_item_with_existing_item(cx: &mut TestAppContext) {
2874        init_test(cx);
2875        let fs = FakeFs::new(cx.executor());
2876
2877        let project = Project::test(fs, None, cx).await;
2878        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
2879        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
2880
2881        // 1. Add with a destination index
2882        //   1a. Add before the active item
2883        let [_, _, _, d] = set_labeled_items(&pane, ["A", "B*", "C", "D"], cx);
2884        pane.update(cx, |pane, cx| {
2885            pane.add_item(d, false, false, Some(0), cx);
2886        });
2887        assert_item_labels(&pane, ["D*", "A", "B", "C"], cx);
2888
2889        //   1b. Add after the active item
2890        let [_, _, _, d] = set_labeled_items(&pane, ["A", "B*", "C", "D"], cx);
2891        pane.update(cx, |pane, cx| {
2892            pane.add_item(d, false, false, Some(2), cx);
2893        });
2894        assert_item_labels(&pane, ["A", "B", "D*", "C"], cx);
2895
2896        //   1c. Add at the end of the item list (including off the length)
2897        let [a, _, _, _] = set_labeled_items(&pane, ["A", "B*", "C", "D"], cx);
2898        pane.update(cx, |pane, cx| {
2899            pane.add_item(a, false, false, Some(5), cx);
2900        });
2901        assert_item_labels(&pane, ["B", "C", "D", "A*"], cx);
2902
2903        //   1d. Add same item to active index
2904        let [_, b, _] = set_labeled_items(&pane, ["A", "B*", "C"], cx);
2905        pane.update(cx, |pane, cx| {
2906            pane.add_item(b, false, false, Some(1), cx);
2907        });
2908        assert_item_labels(&pane, ["A", "B*", "C"], cx);
2909
2910        //   1e. Add item to index after same item in last position
2911        let [_, _, c] = set_labeled_items(&pane, ["A", "B*", "C"], cx);
2912        pane.update(cx, |pane, cx| {
2913            pane.add_item(c, false, false, Some(2), cx);
2914        });
2915        assert_item_labels(&pane, ["A", "B", "C*"], cx);
2916
2917        // 2. Add without a destination index
2918        //   2a. Add with active item at the start of the item list
2919        let [_, _, _, d] = set_labeled_items(&pane, ["A*", "B", "C", "D"], cx);
2920        pane.update(cx, |pane, cx| {
2921            pane.add_item(d, false, false, None, cx);
2922        });
2923        assert_item_labels(&pane, ["A", "D*", "B", "C"], cx);
2924
2925        //   2b. Add with active item at the end of the item list
2926        let [a, _, _, _] = set_labeled_items(&pane, ["A", "B", "C", "D*"], cx);
2927        pane.update(cx, |pane, cx| {
2928            pane.add_item(a, false, false, None, cx);
2929        });
2930        assert_item_labels(&pane, ["B", "C", "D", "A*"], cx);
2931
2932        //   2c. Add active item to active item at end of list
2933        let [_, _, c] = set_labeled_items(&pane, ["A", "B", "C*"], cx);
2934        pane.update(cx, |pane, cx| {
2935            pane.add_item(c, false, false, None, cx);
2936        });
2937        assert_item_labels(&pane, ["A", "B", "C*"], cx);
2938
2939        //   2d. Add active item to active item at start of list
2940        let [a, _, _] = set_labeled_items(&pane, ["A*", "B", "C"], cx);
2941        pane.update(cx, |pane, cx| {
2942            pane.add_item(a, false, false, None, cx);
2943        });
2944        assert_item_labels(&pane, ["A*", "B", "C"], cx);
2945    }
2946
2947    #[gpui::test]
2948    async fn test_add_item_with_same_project_entries(cx: &mut TestAppContext) {
2949        init_test(cx);
2950        let fs = FakeFs::new(cx.executor());
2951
2952        let project = Project::test(fs, None, cx).await;
2953        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
2954        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
2955
2956        // singleton view
2957        pane.update(cx, |pane, cx| {
2958            pane.add_item(
2959                Box::new(cx.new_view(|cx| {
2960                    TestItem::new(cx)
2961                        .with_singleton(true)
2962                        .with_label("buffer 1")
2963                        .with_project_items(&[TestProjectItem::new(1, "one.txt", cx)])
2964                })),
2965                false,
2966                false,
2967                None,
2968                cx,
2969            );
2970        });
2971        assert_item_labels(&pane, ["buffer 1*"], cx);
2972
2973        // new singleton view with the same project entry
2974        pane.update(cx, |pane, cx| {
2975            pane.add_item(
2976                Box::new(cx.new_view(|cx| {
2977                    TestItem::new(cx)
2978                        .with_singleton(true)
2979                        .with_label("buffer 1")
2980                        .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
2981                })),
2982                false,
2983                false,
2984                None,
2985                cx,
2986            );
2987        });
2988        assert_item_labels(&pane, ["buffer 1*"], cx);
2989
2990        // new singleton view with different project entry
2991        pane.update(cx, |pane, cx| {
2992            pane.add_item(
2993                Box::new(cx.new_view(|cx| {
2994                    TestItem::new(cx)
2995                        .with_singleton(true)
2996                        .with_label("buffer 2")
2997                        .with_project_items(&[TestProjectItem::new(2, "2.txt", cx)])
2998                })),
2999                false,
3000                false,
3001                None,
3002                cx,
3003            );
3004        });
3005        assert_item_labels(&pane, ["buffer 1", "buffer 2*"], cx);
3006
3007        // new multibuffer view with the same project entry
3008        pane.update(cx, |pane, cx| {
3009            pane.add_item(
3010                Box::new(cx.new_view(|cx| {
3011                    TestItem::new(cx)
3012                        .with_singleton(false)
3013                        .with_label("multibuffer 1")
3014                        .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
3015                })),
3016                false,
3017                false,
3018                None,
3019                cx,
3020            );
3021        });
3022        assert_item_labels(&pane, ["buffer 1", "buffer 2", "multibuffer 1*"], cx);
3023
3024        // another multibuffer view with the same project entry
3025        pane.update(cx, |pane, cx| {
3026            pane.add_item(
3027                Box::new(cx.new_view(|cx| {
3028                    TestItem::new(cx)
3029                        .with_singleton(false)
3030                        .with_label("multibuffer 1b")
3031                        .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
3032                })),
3033                false,
3034                false,
3035                None,
3036                cx,
3037            );
3038        });
3039        assert_item_labels(
3040            &pane,
3041            ["buffer 1", "buffer 2", "multibuffer 1", "multibuffer 1b*"],
3042            cx,
3043        );
3044    }
3045
3046    #[gpui::test]
3047    async fn test_remove_item_ordering(cx: &mut TestAppContext) {
3048        init_test(cx);
3049        let fs = FakeFs::new(cx.executor());
3050
3051        let project = Project::test(fs, None, cx).await;
3052        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
3053        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3054
3055        add_labeled_item(&pane, "A", false, cx);
3056        add_labeled_item(&pane, "B", false, cx);
3057        add_labeled_item(&pane, "C", false, cx);
3058        add_labeled_item(&pane, "D", false, cx);
3059        assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
3060
3061        pane.update(cx, |pane, cx| pane.activate_item(1, false, false, cx));
3062        add_labeled_item(&pane, "1", false, cx);
3063        assert_item_labels(&pane, ["A", "B", "1*", "C", "D"], cx);
3064
3065        pane.update(cx, |pane, cx| {
3066            pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
3067        })
3068        .unwrap()
3069        .await
3070        .unwrap();
3071        assert_item_labels(&pane, ["A", "B*", "C", "D"], cx);
3072
3073        pane.update(cx, |pane, cx| pane.activate_item(3, false, false, cx));
3074        assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
3075
3076        pane.update(cx, |pane, cx| {
3077            pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
3078        })
3079        .unwrap()
3080        .await
3081        .unwrap();
3082        assert_item_labels(&pane, ["A", "B*", "C"], cx);
3083
3084        pane.update(cx, |pane, cx| {
3085            pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
3086        })
3087        .unwrap()
3088        .await
3089        .unwrap();
3090        assert_item_labels(&pane, ["A", "C*"], cx);
3091
3092        pane.update(cx, |pane, cx| {
3093            pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
3094        })
3095        .unwrap()
3096        .await
3097        .unwrap();
3098        assert_item_labels(&pane, ["A*"], cx);
3099    }
3100
3101    #[gpui::test]
3102    async fn test_close_inactive_items(cx: &mut TestAppContext) {
3103        init_test(cx);
3104        let fs = FakeFs::new(cx.executor());
3105
3106        let project = Project::test(fs, None, cx).await;
3107        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
3108        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3109
3110        set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx);
3111
3112        pane.update(cx, |pane, cx| {
3113            pane.close_inactive_items(&CloseInactiveItems { save_intent: None }, cx)
3114        })
3115        .unwrap()
3116        .await
3117        .unwrap();
3118        assert_item_labels(&pane, ["C*"], cx);
3119    }
3120
3121    #[gpui::test]
3122    async fn test_close_clean_items(cx: &mut TestAppContext) {
3123        init_test(cx);
3124        let fs = FakeFs::new(cx.executor());
3125
3126        let project = Project::test(fs, None, cx).await;
3127        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
3128        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3129
3130        add_labeled_item(&pane, "A", true, cx);
3131        add_labeled_item(&pane, "B", false, cx);
3132        add_labeled_item(&pane, "C", true, cx);
3133        add_labeled_item(&pane, "D", false, cx);
3134        add_labeled_item(&pane, "E", false, cx);
3135        assert_item_labels(&pane, ["A^", "B", "C^", "D", "E*"], cx);
3136
3137        pane.update(cx, |pane, cx| pane.close_clean_items(&CloseCleanItems, cx))
3138            .unwrap()
3139            .await
3140            .unwrap();
3141        assert_item_labels(&pane, ["A^", "C*^"], cx);
3142    }
3143
3144    #[gpui::test]
3145    async fn test_close_items_to_the_left(cx: &mut TestAppContext) {
3146        init_test(cx);
3147        let fs = FakeFs::new(cx.executor());
3148
3149        let project = Project::test(fs, None, cx).await;
3150        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
3151        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3152
3153        set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx);
3154
3155        pane.update(cx, |pane, cx| {
3156            pane.close_items_to_the_left(&CloseItemsToTheLeft, cx)
3157        })
3158        .unwrap()
3159        .await
3160        .unwrap();
3161        assert_item_labels(&pane, ["C*", "D", "E"], cx);
3162    }
3163
3164    #[gpui::test]
3165    async fn test_close_items_to_the_right(cx: &mut TestAppContext) {
3166        init_test(cx);
3167        let fs = FakeFs::new(cx.executor());
3168
3169        let project = Project::test(fs, None, cx).await;
3170        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
3171        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3172
3173        set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx);
3174
3175        pane.update(cx, |pane, cx| {
3176            pane.close_items_to_the_right(&CloseItemsToTheRight, cx)
3177        })
3178        .unwrap()
3179        .await
3180        .unwrap();
3181        assert_item_labels(&pane, ["A", "B", "C*"], cx);
3182    }
3183
3184    #[gpui::test]
3185    async fn test_close_all_items(cx: &mut TestAppContext) {
3186        init_test(cx);
3187        let fs = FakeFs::new(cx.executor());
3188
3189        let project = Project::test(fs, None, cx).await;
3190        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
3191        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3192
3193        add_labeled_item(&pane, "A", false, cx);
3194        add_labeled_item(&pane, "B", false, cx);
3195        add_labeled_item(&pane, "C", false, cx);
3196        assert_item_labels(&pane, ["A", "B", "C*"], cx);
3197
3198        pane.update(cx, |pane, cx| {
3199            pane.close_all_items(&CloseAllItems { save_intent: None }, cx)
3200        })
3201        .unwrap()
3202        .await
3203        .unwrap();
3204        assert_item_labels(&pane, [], cx);
3205
3206        add_labeled_item(&pane, "A", true, cx);
3207        add_labeled_item(&pane, "B", true, cx);
3208        add_labeled_item(&pane, "C", true, cx);
3209        assert_item_labels(&pane, ["A^", "B^", "C*^"], cx);
3210
3211        let save = pane
3212            .update(cx, |pane, cx| {
3213                pane.close_all_items(&CloseAllItems { save_intent: None }, cx)
3214            })
3215            .unwrap();
3216
3217        cx.executor().run_until_parked();
3218        cx.simulate_prompt_answer(2);
3219        save.await.unwrap();
3220        assert_item_labels(&pane, [], cx);
3221    }
3222
3223    fn init_test(cx: &mut TestAppContext) {
3224        cx.update(|cx| {
3225            let settings_store = SettingsStore::test(cx);
3226            cx.set_global(settings_store);
3227            theme::init(LoadThemes::JustBase, cx);
3228            crate::init_settings(cx);
3229            Project::init_settings(cx);
3230        });
3231    }
3232
3233    fn add_labeled_item(
3234        pane: &View<Pane>,
3235        label: &str,
3236        is_dirty: bool,
3237        cx: &mut VisualTestContext,
3238    ) -> Box<View<TestItem>> {
3239        pane.update(cx, |pane, cx| {
3240            let labeled_item = Box::new(
3241                cx.new_view(|cx| TestItem::new(cx).with_label(label).with_dirty(is_dirty)),
3242            );
3243            pane.add_item(labeled_item.clone(), false, false, None, cx);
3244            labeled_item
3245        })
3246    }
3247
3248    fn set_labeled_items<const COUNT: usize>(
3249        pane: &View<Pane>,
3250        labels: [&str; COUNT],
3251        cx: &mut VisualTestContext,
3252    ) -> [Box<View<TestItem>>; COUNT] {
3253        pane.update(cx, |pane, cx| {
3254            pane.items.clear();
3255            let mut active_item_index = 0;
3256
3257            let mut index = 0;
3258            let items = labels.map(|mut label| {
3259                if label.ends_with('*') {
3260                    label = label.trim_end_matches('*');
3261                    active_item_index = index;
3262                }
3263
3264                let labeled_item = Box::new(cx.new_view(|cx| TestItem::new(cx).with_label(label)));
3265                pane.add_item(labeled_item.clone(), false, false, None, cx);
3266                index += 1;
3267                labeled_item
3268            });
3269
3270            pane.activate_item(active_item_index, false, false, cx);
3271
3272            items
3273        })
3274    }
3275
3276    // Assert the item label, with the active item label suffixed with a '*'
3277    fn assert_item_labels<const COUNT: usize>(
3278        pane: &View<Pane>,
3279        expected_states: [&str; COUNT],
3280        cx: &mut VisualTestContext,
3281    ) {
3282        pane.update(cx, |pane, cx| {
3283            let actual_states = pane
3284                .items
3285                .iter()
3286                .enumerate()
3287                .map(|(ix, item)| {
3288                    let mut state = item
3289                        .to_any()
3290                        .downcast::<TestItem>()
3291                        .unwrap()
3292                        .read(cx)
3293                        .label
3294                        .clone();
3295                    if ix == pane.active_item_index {
3296                        state.push('*');
3297                    }
3298                    if item.is_dirty(cx) {
3299                        state.push('^');
3300                    }
3301                    state
3302                })
3303                .collect::<Vec<_>>();
3304
3305            assert_eq!(
3306                actual_states, expected_states,
3307                "pane items do not match expectation"
3308            );
3309        })
3310    }
3311}
3312
3313impl Render for DraggedTab {
3314    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
3315        let ui_font = ThemeSettings::get_global(cx).ui_font.clone();
3316        let label = self.item.tab_content(
3317            TabContentParams {
3318                detail: Some(self.detail),
3319                selected: false,
3320                preview: false,
3321            },
3322            cx,
3323        );
3324        Tab::new("")
3325            .selected(self.is_active)
3326            .child(label)
3327            .render(cx)
3328            .font(ui_font)
3329    }
3330}