pane.rs

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