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