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.id("pane_placeholder")
3319                                .v_flex()
3320                                .size_full()
3321                                .overflow_hidden()
3322                                .child(self.toolbar.clone())
3323                                .child(item.to_any())
3324                        } else {
3325                            let placeholder = div
3326                                .id("pane_placeholder")
3327                                .h_flex()
3328                                .size_full()
3329                                .justify_center()
3330                                .on_click(cx.listener(
3331                                    move |this, event: &ClickEvent, window, cx| {
3332                                        if event.up.click_count == 2 {
3333                                            window.dispatch_action(
3334                                                this.double_click_dispatch_action.boxed_clone(),
3335                                                cx,
3336                                            );
3337                                        }
3338                                    },
3339                                ));
3340                            if has_worktrees {
3341                                placeholder
3342                            } else {
3343                                placeholder.child(
3344                                    Label::new("Open a file or project to get started.")
3345                                        .color(Color::Muted),
3346                                )
3347                            }
3348                        }
3349                    })
3350                    .child(
3351                        // drag target
3352                        div()
3353                            .invisible()
3354                            .absolute()
3355                            .bg(cx.theme().colors().drop_target_background)
3356                            .group_drag_over::<DraggedTab>("", |style| style.visible())
3357                            .group_drag_over::<DraggedSelection>("", |style| style.visible())
3358                            .when(is_local, |div| {
3359                                div.group_drag_over::<ExternalPaths>("", |style| style.visible())
3360                            })
3361                            .when_some(self.can_drop_predicate.clone(), |this, p| {
3362                                this.can_drop(move |a, window, cx| p(a, window, cx))
3363                            })
3364                            .on_drop(cx.listener(move |this, dragged_tab, window, cx| {
3365                                this.handle_tab_drop(
3366                                    dragged_tab,
3367                                    this.active_item_index(),
3368                                    window,
3369                                    cx,
3370                                )
3371                            }))
3372                            .on_drop(cx.listener(
3373                                move |this, selection: &DraggedSelection, window, cx| {
3374                                    this.handle_dragged_selection_drop(selection, None, window, cx)
3375                                },
3376                            ))
3377                            .on_drop(cx.listener(move |this, paths, window, cx| {
3378                                this.handle_external_paths_drop(paths, window, cx)
3379                            }))
3380                            .map(|div| {
3381                                let size = DefiniteLength::Fraction(0.5);
3382                                match self.drag_split_direction {
3383                                    None => div.top_0().right_0().bottom_0().left_0(),
3384                                    Some(SplitDirection::Up) => {
3385                                        div.top_0().left_0().right_0().h(size)
3386                                    }
3387                                    Some(SplitDirection::Down) => {
3388                                        div.left_0().bottom_0().right_0().h(size)
3389                                    }
3390                                    Some(SplitDirection::Left) => {
3391                                        div.top_0().left_0().bottom_0().w(size)
3392                                    }
3393                                    Some(SplitDirection::Right) => {
3394                                        div.top_0().bottom_0().right_0().w(size)
3395                                    }
3396                                }
3397                            }),
3398                    )
3399            })
3400            .on_mouse_down(
3401                MouseButton::Navigate(NavigationDirection::Back),
3402                cx.listener(|pane, _, window, cx| {
3403                    if let Some(workspace) = pane.workspace.upgrade() {
3404                        let pane = cx.entity().downgrade();
3405                        window.defer(cx, move |window, cx| {
3406                            workspace.update(cx, |workspace, cx| {
3407                                workspace.go_back(pane, window, cx).detach_and_log_err(cx)
3408                            })
3409                        })
3410                    }
3411                }),
3412            )
3413            .on_mouse_down(
3414                MouseButton::Navigate(NavigationDirection::Forward),
3415                cx.listener(|pane, _, window, cx| {
3416                    if let Some(workspace) = pane.workspace.upgrade() {
3417                        let pane = cx.entity().downgrade();
3418                        window.defer(cx, move |window, cx| {
3419                            workspace.update(cx, |workspace, cx| {
3420                                workspace
3421                                    .go_forward(pane, window, cx)
3422                                    .detach_and_log_err(cx)
3423                            })
3424                        })
3425                    }
3426                }),
3427            )
3428    }
3429}
3430
3431impl ItemNavHistory {
3432    pub fn push<D: 'static + Send + Any>(&mut self, data: Option<D>, cx: &mut App) {
3433        if self
3434            .item
3435            .upgrade()
3436            .is_some_and(|item| item.include_in_nav_history())
3437        {
3438            self.history
3439                .push(data, self.item.clone(), self.is_preview, cx);
3440        }
3441    }
3442
3443    pub fn pop_backward(&mut self, cx: &mut App) -> Option<NavigationEntry> {
3444        self.history.pop(NavigationMode::GoingBack, cx)
3445    }
3446
3447    pub fn pop_forward(&mut self, cx: &mut App) -> Option<NavigationEntry> {
3448        self.history.pop(NavigationMode::GoingForward, cx)
3449    }
3450}
3451
3452impl NavHistory {
3453    pub fn for_each_entry(
3454        &self,
3455        cx: &App,
3456        mut f: impl FnMut(&NavigationEntry, (ProjectPath, Option<PathBuf>)),
3457    ) {
3458        let borrowed_history = self.0.lock();
3459        borrowed_history
3460            .forward_stack
3461            .iter()
3462            .chain(borrowed_history.backward_stack.iter())
3463            .chain(borrowed_history.closed_stack.iter())
3464            .for_each(|entry| {
3465                if let Some(project_and_abs_path) =
3466                    borrowed_history.paths_by_item.get(&entry.item.id())
3467                {
3468                    f(entry, project_and_abs_path.clone());
3469                } else if let Some(item) = entry.item.upgrade() {
3470                    if let Some(path) = item.project_path(cx) {
3471                        f(entry, (path, None));
3472                    }
3473                }
3474            })
3475    }
3476
3477    pub fn set_mode(&mut self, mode: NavigationMode) {
3478        self.0.lock().mode = mode;
3479    }
3480
3481    pub fn mode(&self) -> NavigationMode {
3482        self.0.lock().mode
3483    }
3484
3485    pub fn disable(&mut self) {
3486        self.0.lock().mode = NavigationMode::Disabled;
3487    }
3488
3489    pub fn enable(&mut self) {
3490        self.0.lock().mode = NavigationMode::Normal;
3491    }
3492
3493    pub fn pop(&mut self, mode: NavigationMode, cx: &mut App) -> Option<NavigationEntry> {
3494        let mut state = self.0.lock();
3495        let entry = match mode {
3496            NavigationMode::Normal | NavigationMode::Disabled | NavigationMode::ClosingItem => {
3497                return None
3498            }
3499            NavigationMode::GoingBack => &mut state.backward_stack,
3500            NavigationMode::GoingForward => &mut state.forward_stack,
3501            NavigationMode::ReopeningClosedItem => &mut state.closed_stack,
3502        }
3503        .pop_back();
3504        if entry.is_some() {
3505            state.did_update(cx);
3506        }
3507        entry
3508    }
3509
3510    pub fn push<D: 'static + Send + Any>(
3511        &mut self,
3512        data: Option<D>,
3513        item: Arc<dyn WeakItemHandle>,
3514        is_preview: bool,
3515        cx: &mut App,
3516    ) {
3517        let state = &mut *self.0.lock();
3518        match state.mode {
3519            NavigationMode::Disabled => {}
3520            NavigationMode::Normal | NavigationMode::ReopeningClosedItem => {
3521                if state.backward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
3522                    state.backward_stack.pop_front();
3523                }
3524                state.backward_stack.push_back(NavigationEntry {
3525                    item,
3526                    data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
3527                    timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
3528                    is_preview,
3529                });
3530                state.forward_stack.clear();
3531            }
3532            NavigationMode::GoingBack => {
3533                if state.forward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
3534                    state.forward_stack.pop_front();
3535                }
3536                state.forward_stack.push_back(NavigationEntry {
3537                    item,
3538                    data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
3539                    timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
3540                    is_preview,
3541                });
3542            }
3543            NavigationMode::GoingForward => {
3544                if state.backward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
3545                    state.backward_stack.pop_front();
3546                }
3547                state.backward_stack.push_back(NavigationEntry {
3548                    item,
3549                    data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
3550                    timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
3551                    is_preview,
3552                });
3553            }
3554            NavigationMode::ClosingItem => {
3555                if state.closed_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
3556                    state.closed_stack.pop_front();
3557                }
3558                state.closed_stack.push_back(NavigationEntry {
3559                    item,
3560                    data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
3561                    timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
3562                    is_preview,
3563                });
3564            }
3565        }
3566        state.did_update(cx);
3567    }
3568
3569    pub fn remove_item(&mut self, item_id: EntityId) {
3570        let mut state = self.0.lock();
3571        state.paths_by_item.remove(&item_id);
3572        state
3573            .backward_stack
3574            .retain(|entry| entry.item.id() != item_id);
3575        state
3576            .forward_stack
3577            .retain(|entry| entry.item.id() != item_id);
3578        state
3579            .closed_stack
3580            .retain(|entry| entry.item.id() != item_id);
3581    }
3582
3583    pub fn path_for_item(&self, item_id: EntityId) -> Option<(ProjectPath, Option<PathBuf>)> {
3584        self.0.lock().paths_by_item.get(&item_id).cloned()
3585    }
3586}
3587
3588impl NavHistoryState {
3589    pub fn did_update(&self, cx: &mut App) {
3590        if let Some(pane) = self.pane.upgrade() {
3591            cx.defer(move |cx| {
3592                pane.update(cx, |pane, cx| pane.history_updated(cx));
3593            });
3594        }
3595    }
3596}
3597
3598fn dirty_message_for(buffer_path: Option<ProjectPath>) -> String {
3599    let path = buffer_path
3600        .as_ref()
3601        .and_then(|p| {
3602            p.path
3603                .to_str()
3604                .and_then(|s| if s.is_empty() { None } else { Some(s) })
3605        })
3606        .unwrap_or("This buffer");
3607    let path = truncate_and_remove_front(path, 80);
3608    format!("{path} contains unsaved edits. Do you want to save it?")
3609}
3610
3611pub fn tab_details(items: &[Box<dyn ItemHandle>], cx: &App) -> Vec<usize> {
3612    let mut tab_details = items.iter().map(|_| 0).collect::<Vec<_>>();
3613    let mut tab_descriptions = HashMap::default();
3614    let mut done = false;
3615    while !done {
3616        done = true;
3617
3618        // Store item indices by their tab description.
3619        for (ix, (item, detail)) in items.iter().zip(&tab_details).enumerate() {
3620            if let Some(description) = item.tab_description(*detail, cx) {
3621                if *detail == 0
3622                    || Some(&description) != item.tab_description(detail - 1, cx).as_ref()
3623                {
3624                    tab_descriptions
3625                        .entry(description)
3626                        .or_insert(Vec::new())
3627                        .push(ix);
3628                }
3629            }
3630        }
3631
3632        // If two or more items have the same tab description, increase their level
3633        // of detail and try again.
3634        for (_, item_ixs) in tab_descriptions.drain() {
3635            if item_ixs.len() > 1 {
3636                done = false;
3637                for ix in item_ixs {
3638                    tab_details[ix] += 1;
3639                }
3640            }
3641        }
3642    }
3643
3644    tab_details
3645}
3646
3647pub fn render_item_indicator(item: Box<dyn ItemHandle>, cx: &App) -> Option<Indicator> {
3648    maybe!({
3649        let indicator_color = match (item.has_conflict(cx), item.is_dirty(cx)) {
3650            (true, _) => Color::Warning,
3651            (_, true) => Color::Accent,
3652            (false, false) => return None,
3653        };
3654
3655        Some(Indicator::dot().color(indicator_color))
3656    })
3657}
3658
3659impl Render for DraggedTab {
3660    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
3661        let ui_font = ThemeSettings::get_global(cx).ui_font.clone();
3662        let label = self.item.tab_content(
3663            TabContentParams {
3664                detail: Some(self.detail),
3665                selected: false,
3666                preview: false,
3667            },
3668            window,
3669            cx,
3670        );
3671        Tab::new("")
3672            .toggle_state(self.is_active)
3673            .child(label)
3674            .render(window, cx)
3675            .font(ui_font)
3676    }
3677}
3678
3679#[cfg(test)]
3680mod tests {
3681    use std::num::NonZero;
3682
3683    use super::*;
3684    use crate::item::test::{TestItem, TestProjectItem};
3685    use gpui::{TestAppContext, VisualTestContext};
3686    use project::FakeFs;
3687    use settings::SettingsStore;
3688    use theme::LoadThemes;
3689
3690    #[gpui::test]
3691    async fn test_remove_active_empty(cx: &mut TestAppContext) {
3692        init_test(cx);
3693        let fs = FakeFs::new(cx.executor());
3694
3695        let project = Project::test(fs, None, cx).await;
3696        let (workspace, cx) =
3697            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
3698        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3699
3700        pane.update_in(cx, |pane, window, cx| {
3701            assert!(pane
3702                .close_active_item(
3703                    &CloseActiveItem {
3704                        save_intent: None,
3705                        close_pinned: false
3706                    },
3707                    window,
3708                    cx
3709                )
3710                .is_none())
3711        });
3712    }
3713
3714    #[gpui::test]
3715    async fn test_add_item_capped_to_max_tabs(cx: &mut TestAppContext) {
3716        init_test(cx);
3717        let fs = FakeFs::new(cx.executor());
3718
3719        let project = Project::test(fs, None, cx).await;
3720        let (workspace, cx) =
3721            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
3722        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3723
3724        for i in 0..7 {
3725            add_labeled_item(&pane, format!("{}", i).as_str(), false, cx);
3726        }
3727        set_max_tabs(cx, Some(5));
3728        add_labeled_item(&pane, "7", false, cx);
3729        // Remove items to respect the max tab cap.
3730        assert_item_labels(&pane, ["3", "4", "5", "6", "7*"], cx);
3731        pane.update_in(cx, |pane, window, cx| {
3732            pane.activate_item(0, false, false, window, cx);
3733        });
3734        add_labeled_item(&pane, "X", false, cx);
3735        // Respect activation order.
3736        assert_item_labels(&pane, ["3", "X*", "5", "6", "7"], cx);
3737
3738        for i in 0..7 {
3739            add_labeled_item(&pane, format!("D{}", i).as_str(), true, cx);
3740        }
3741        // Keeps dirty items, even over max tab cap.
3742        assert_item_labels(
3743            &pane,
3744            ["D0^", "D1^", "D2^", "D3^", "D4^", "D5^", "D6*^"],
3745            cx,
3746        );
3747
3748        set_max_tabs(cx, None);
3749        for i in 0..7 {
3750            add_labeled_item(&pane, format!("N{}", i).as_str(), false, cx);
3751        }
3752        // No cap when max tabs is None.
3753        assert_item_labels(
3754            &pane,
3755            [
3756                "D0^", "D1^", "D2^", "D3^", "D4^", "D5^", "D6^", "N0", "N1", "N2", "N3", "N4",
3757                "N5", "N6*",
3758            ],
3759            cx,
3760        );
3761    }
3762
3763    #[gpui::test]
3764    async fn test_add_item_with_new_item(cx: &mut TestAppContext) {
3765        init_test(cx);
3766        let fs = FakeFs::new(cx.executor());
3767
3768        let project = Project::test(fs, None, cx).await;
3769        let (workspace, cx) =
3770            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
3771        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3772
3773        // 1. Add with a destination index
3774        //   a. Add before the active item
3775        set_labeled_items(&pane, ["A", "B*", "C"], cx);
3776        pane.update_in(cx, |pane, window, cx| {
3777            pane.add_item(
3778                Box::new(cx.new(|cx| TestItem::new(cx).with_label("D"))),
3779                false,
3780                false,
3781                Some(0),
3782                window,
3783                cx,
3784            );
3785        });
3786        assert_item_labels(&pane, ["D*", "A", "B", "C"], cx);
3787
3788        //   b. Add after the active item
3789        set_labeled_items(&pane, ["A", "B*", "C"], cx);
3790        pane.update_in(cx, |pane, window, cx| {
3791            pane.add_item(
3792                Box::new(cx.new(|cx| TestItem::new(cx).with_label("D"))),
3793                false,
3794                false,
3795                Some(2),
3796                window,
3797                cx,
3798            );
3799        });
3800        assert_item_labels(&pane, ["A", "B", "D*", "C"], cx);
3801
3802        //   c. Add at the end of the item list (including off the length)
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                Some(5),
3810                window,
3811                cx,
3812            );
3813        });
3814        assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
3815
3816        // 2. Add without a destination index
3817        //   a. Add with active item at the start of the item list
3818        set_labeled_items(&pane, ["A*", "B", "C"], cx);
3819        pane.update_in(cx, |pane, window, cx| {
3820            pane.add_item(
3821                Box::new(cx.new(|cx| TestItem::new(cx).with_label("D"))),
3822                false,
3823                false,
3824                None,
3825                window,
3826                cx,
3827            );
3828        });
3829        set_labeled_items(&pane, ["A", "D*", "B", "C"], cx);
3830
3831        //   b. Add with active item at the end of the item list
3832        set_labeled_items(&pane, ["A", "B", "C*"], cx);
3833        pane.update_in(cx, |pane, window, cx| {
3834            pane.add_item(
3835                Box::new(cx.new(|cx| TestItem::new(cx).with_label("D"))),
3836                false,
3837                false,
3838                None,
3839                window,
3840                cx,
3841            );
3842        });
3843        assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
3844    }
3845
3846    #[gpui::test]
3847    async fn test_add_item_with_existing_item(cx: &mut TestAppContext) {
3848        init_test(cx);
3849        let fs = FakeFs::new(cx.executor());
3850
3851        let project = Project::test(fs, None, cx).await;
3852        let (workspace, cx) =
3853            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
3854        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3855
3856        // 1. Add with a destination index
3857        //   1a. Add before the active item
3858        let [_, _, _, d] = set_labeled_items(&pane, ["A", "B*", "C", "D"], cx);
3859        pane.update_in(cx, |pane, window, cx| {
3860            pane.add_item(d, false, false, Some(0), window, cx);
3861        });
3862        assert_item_labels(&pane, ["D*", "A", "B", "C"], cx);
3863
3864        //   1b. Add after the active item
3865        let [_, _, _, d] = set_labeled_items(&pane, ["A", "B*", "C", "D"], cx);
3866        pane.update_in(cx, |pane, window, cx| {
3867            pane.add_item(d, false, false, Some(2), window, cx);
3868        });
3869        assert_item_labels(&pane, ["A", "B", "D*", "C"], cx);
3870
3871        //   1c. Add at the end of the item list (including off the length)
3872        let [a, _, _, _] = set_labeled_items(&pane, ["A", "B*", "C", "D"], cx);
3873        pane.update_in(cx, |pane, window, cx| {
3874            pane.add_item(a, false, false, Some(5), window, cx);
3875        });
3876        assert_item_labels(&pane, ["B", "C", "D", "A*"], cx);
3877
3878        //   1d. Add same item to active index
3879        let [_, b, _] = set_labeled_items(&pane, ["A", "B*", "C"], cx);
3880        pane.update_in(cx, |pane, window, cx| {
3881            pane.add_item(b, false, false, Some(1), window, cx);
3882        });
3883        assert_item_labels(&pane, ["A", "B*", "C"], cx);
3884
3885        //   1e. Add item to index after same item in last position
3886        let [_, _, c] = set_labeled_items(&pane, ["A", "B*", "C"], cx);
3887        pane.update_in(cx, |pane, window, cx| {
3888            pane.add_item(c, false, false, Some(2), window, cx);
3889        });
3890        assert_item_labels(&pane, ["A", "B", "C*"], cx);
3891
3892        // 2. Add without a destination index
3893        //   2a. Add with active item at the start of the item list
3894        let [_, _, _, d] = set_labeled_items(&pane, ["A*", "B", "C", "D"], cx);
3895        pane.update_in(cx, |pane, window, cx| {
3896            pane.add_item(d, false, false, None, window, cx);
3897        });
3898        assert_item_labels(&pane, ["A", "D*", "B", "C"], cx);
3899
3900        //   2b. Add with active item at the end of the item list
3901        let [a, _, _, _] = set_labeled_items(&pane, ["A", "B", "C", "D*"], cx);
3902        pane.update_in(cx, |pane, window, cx| {
3903            pane.add_item(a, false, false, None, window, cx);
3904        });
3905        assert_item_labels(&pane, ["B", "C", "D", "A*"], cx);
3906
3907        //   2c. Add active item to active item at end of list
3908        let [_, _, c] = set_labeled_items(&pane, ["A", "B", "C*"], cx);
3909        pane.update_in(cx, |pane, window, cx| {
3910            pane.add_item(c, false, false, None, window, cx);
3911        });
3912        assert_item_labels(&pane, ["A", "B", "C*"], cx);
3913
3914        //   2d. Add active item to active item at start of list
3915        let [a, _, _] = set_labeled_items(&pane, ["A*", "B", "C"], cx);
3916        pane.update_in(cx, |pane, window, cx| {
3917            pane.add_item(a, false, false, None, window, cx);
3918        });
3919        assert_item_labels(&pane, ["A*", "B", "C"], cx);
3920    }
3921
3922    #[gpui::test]
3923    async fn test_add_item_with_same_project_entries(cx: &mut TestAppContext) {
3924        init_test(cx);
3925        let fs = FakeFs::new(cx.executor());
3926
3927        let project = Project::test(fs, None, cx).await;
3928        let (workspace, cx) =
3929            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
3930        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3931
3932        // singleton view
3933        pane.update_in(cx, |pane, window, cx| {
3934            pane.add_item(
3935                Box::new(cx.new(|cx| {
3936                    TestItem::new(cx)
3937                        .with_singleton(true)
3938                        .with_label("buffer 1")
3939                        .with_project_items(&[TestProjectItem::new(1, "one.txt", cx)])
3940                })),
3941                false,
3942                false,
3943                None,
3944                window,
3945                cx,
3946            );
3947        });
3948        assert_item_labels(&pane, ["buffer 1*"], cx);
3949
3950        // new singleton view with the same project entry
3951        pane.update_in(cx, |pane, window, cx| {
3952            pane.add_item(
3953                Box::new(cx.new(|cx| {
3954                    TestItem::new(cx)
3955                        .with_singleton(true)
3956                        .with_label("buffer 1")
3957                        .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
3958                })),
3959                false,
3960                false,
3961                None,
3962                window,
3963                cx,
3964            );
3965        });
3966        assert_item_labels(&pane, ["buffer 1*"], cx);
3967
3968        // new singleton view with different project entry
3969        pane.update_in(cx, |pane, window, cx| {
3970            pane.add_item(
3971                Box::new(cx.new(|cx| {
3972                    TestItem::new(cx)
3973                        .with_singleton(true)
3974                        .with_label("buffer 2")
3975                        .with_project_items(&[TestProjectItem::new(2, "2.txt", cx)])
3976                })),
3977                false,
3978                false,
3979                None,
3980                window,
3981                cx,
3982            );
3983        });
3984        assert_item_labels(&pane, ["buffer 1", "buffer 2*"], cx);
3985
3986        // new multibuffer view with the same project entry
3987        pane.update_in(cx, |pane, window, cx| {
3988            pane.add_item(
3989                Box::new(cx.new(|cx| {
3990                    TestItem::new(cx)
3991                        .with_singleton(false)
3992                        .with_label("multibuffer 1")
3993                        .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
3994                })),
3995                false,
3996                false,
3997                None,
3998                window,
3999                cx,
4000            );
4001        });
4002        assert_item_labels(&pane, ["buffer 1", "buffer 2", "multibuffer 1*"], cx);
4003
4004        // another multibuffer view with the same project entry
4005        pane.update_in(cx, |pane, window, cx| {
4006            pane.add_item(
4007                Box::new(cx.new(|cx| {
4008                    TestItem::new(cx)
4009                        .with_singleton(false)
4010                        .with_label("multibuffer 1b")
4011                        .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
4012                })),
4013                false,
4014                false,
4015                None,
4016                window,
4017                cx,
4018            );
4019        });
4020        assert_item_labels(
4021            &pane,
4022            ["buffer 1", "buffer 2", "multibuffer 1", "multibuffer 1b*"],
4023            cx,
4024        );
4025    }
4026
4027    #[gpui::test]
4028    async fn test_remove_item_ordering_history(cx: &mut TestAppContext) {
4029        init_test(cx);
4030        let fs = FakeFs::new(cx.executor());
4031
4032        let project = Project::test(fs, None, cx).await;
4033        let (workspace, cx) =
4034            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4035        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
4036
4037        add_labeled_item(&pane, "A", false, cx);
4038        add_labeled_item(&pane, "B", false, cx);
4039        add_labeled_item(&pane, "C", false, cx);
4040        add_labeled_item(&pane, "D", false, cx);
4041        assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
4042
4043        pane.update_in(cx, |pane, window, cx| {
4044            pane.activate_item(1, false, false, window, cx)
4045        });
4046        add_labeled_item(&pane, "1", false, cx);
4047        assert_item_labels(&pane, ["A", "B", "1*", "C", "D"], cx);
4048
4049        pane.update_in(cx, |pane, window, cx| {
4050            pane.close_active_item(
4051                &CloseActiveItem {
4052                    save_intent: None,
4053                    close_pinned: false,
4054                },
4055                window,
4056                cx,
4057            )
4058        })
4059        .unwrap()
4060        .await
4061        .unwrap();
4062        assert_item_labels(&pane, ["A", "B*", "C", "D"], cx);
4063
4064        pane.update_in(cx, |pane, window, cx| {
4065            pane.activate_item(3, false, false, window, cx)
4066        });
4067        assert_item_labels(&pane, ["A", "B", "C", "D*"], 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", "B*", "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", "C*"], cx);
4098
4099        pane.update_in(cx, |pane, window, cx| {
4100            pane.close_active_item(
4101                &CloseActiveItem {
4102                    save_intent: None,
4103                    close_pinned: false,
4104                },
4105                window,
4106                cx,
4107            )
4108        })
4109        .unwrap()
4110        .await
4111        .unwrap();
4112        assert_item_labels(&pane, ["A*"], cx);
4113    }
4114
4115    #[gpui::test]
4116    async fn test_remove_item_ordering_neighbour(cx: &mut TestAppContext) {
4117        init_test(cx);
4118        cx.update_global::<SettingsStore, ()>(|s, cx| {
4119            s.update_user_settings::<ItemSettings>(cx, |s| {
4120                s.activate_on_close = Some(ActivateOnClose::Neighbour);
4121            });
4122        });
4123        let fs = FakeFs::new(cx.executor());
4124
4125        let project = Project::test(fs, None, cx).await;
4126        let (workspace, cx) =
4127            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4128        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
4129
4130        add_labeled_item(&pane, "A", false, cx);
4131        add_labeled_item(&pane, "B", false, cx);
4132        add_labeled_item(&pane, "C", false, cx);
4133        add_labeled_item(&pane, "D", false, cx);
4134        assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
4135
4136        pane.update_in(cx, |pane, window, cx| {
4137            pane.activate_item(1, false, false, window, cx)
4138        });
4139        add_labeled_item(&pane, "1", false, cx);
4140        assert_item_labels(&pane, ["A", "B", "1*", "C", "D"], cx);
4141
4142        pane.update_in(cx, |pane, window, cx| {
4143            pane.close_active_item(
4144                &CloseActiveItem {
4145                    save_intent: None,
4146                    close_pinned: false,
4147                },
4148                window,
4149                cx,
4150            )
4151        })
4152        .unwrap()
4153        .await
4154        .unwrap();
4155        assert_item_labels(&pane, ["A", "B", "C*", "D"], cx);
4156
4157        pane.update_in(cx, |pane, window, cx| {
4158            pane.activate_item(3, false, false, window, cx)
4159        });
4160        assert_item_labels(&pane, ["A", "B", "C", "D*"], 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", "C*"], 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", "B*"], cx);
4191
4192        pane.update_in(cx, |pane, window, cx| {
4193            pane.close_active_item(
4194                &CloseActiveItem {
4195                    save_intent: None,
4196                    close_pinned: false,
4197                },
4198                window,
4199                cx,
4200            )
4201        })
4202        .unwrap()
4203        .await
4204        .unwrap();
4205        assert_item_labels(&pane, ["A*"], cx);
4206    }
4207
4208    #[gpui::test]
4209    async fn test_remove_item_ordering_left_neighbour(cx: &mut TestAppContext) {
4210        init_test(cx);
4211        cx.update_global::<SettingsStore, ()>(|s, cx| {
4212            s.update_user_settings::<ItemSettings>(cx, |s| {
4213                s.activate_on_close = Some(ActivateOnClose::LeftNeighbour);
4214            });
4215        });
4216        let fs = FakeFs::new(cx.executor());
4217
4218        let project = Project::test(fs, None, cx).await;
4219        let (workspace, cx) =
4220            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4221        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
4222
4223        add_labeled_item(&pane, "A", false, cx);
4224        add_labeled_item(&pane, "B", false, cx);
4225        add_labeled_item(&pane, "C", false, cx);
4226        add_labeled_item(&pane, "D", false, cx);
4227        assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
4228
4229        pane.update_in(cx, |pane, window, cx| {
4230            pane.activate_item(1, false, false, window, cx)
4231        });
4232        add_labeled_item(&pane, "1", false, cx);
4233        assert_item_labels(&pane, ["A", "B", "1*", "C", "D"], cx);
4234
4235        pane.update_in(cx, |pane, window, cx| {
4236            pane.close_active_item(
4237                &CloseActiveItem {
4238                    save_intent: None,
4239                    close_pinned: false,
4240                },
4241                window,
4242                cx,
4243            )
4244        })
4245        .unwrap()
4246        .await
4247        .unwrap();
4248        assert_item_labels(&pane, ["A", "B*", "C", "D"], cx);
4249
4250        pane.update_in(cx, |pane, window, cx| {
4251            pane.activate_item(3, false, false, window, cx)
4252        });
4253        assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
4254
4255        pane.update_in(cx, |pane, window, cx| {
4256            pane.close_active_item(
4257                &CloseActiveItem {
4258                    save_intent: None,
4259                    close_pinned: false,
4260                },
4261                window,
4262                cx,
4263            )
4264        })
4265        .unwrap()
4266        .await
4267        .unwrap();
4268        assert_item_labels(&pane, ["A", "B", "C*"], cx);
4269
4270        pane.update_in(cx, |pane, window, cx| {
4271            pane.activate_item(0, false, false, window, cx)
4272        });
4273        assert_item_labels(&pane, ["A*", "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, ["B*", "C"], cx);
4289
4290        pane.update_in(cx, |pane, window, cx| {
4291            pane.close_active_item(
4292                &CloseActiveItem {
4293                    save_intent: None,
4294                    close_pinned: false,
4295                },
4296                window,
4297                cx,
4298            )
4299        })
4300        .unwrap()
4301        .await
4302        .unwrap();
4303        assert_item_labels(&pane, ["C*"], cx);
4304    }
4305
4306    #[gpui::test]
4307    async fn test_close_inactive_items(cx: &mut TestAppContext) {
4308        init_test(cx);
4309        let fs = FakeFs::new(cx.executor());
4310
4311        let project = Project::test(fs, None, cx).await;
4312        let (workspace, cx) =
4313            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4314        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
4315
4316        set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx);
4317
4318        pane.update_in(cx, |pane, window, cx| {
4319            pane.close_inactive_items(
4320                &CloseInactiveItems {
4321                    save_intent: None,
4322                    close_pinned: false,
4323                },
4324                window,
4325                cx,
4326            )
4327        })
4328        .unwrap()
4329        .await
4330        .unwrap();
4331        assert_item_labels(&pane, ["C*"], cx);
4332    }
4333
4334    #[gpui::test]
4335    async fn test_close_clean_items(cx: &mut TestAppContext) {
4336        init_test(cx);
4337        let fs = FakeFs::new(cx.executor());
4338
4339        let project = Project::test(fs, None, cx).await;
4340        let (workspace, cx) =
4341            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4342        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
4343
4344        add_labeled_item(&pane, "A", true, cx);
4345        add_labeled_item(&pane, "B", false, cx);
4346        add_labeled_item(&pane, "C", true, cx);
4347        add_labeled_item(&pane, "D", false, cx);
4348        add_labeled_item(&pane, "E", false, cx);
4349        assert_item_labels(&pane, ["A^", "B", "C^", "D", "E*"], cx);
4350
4351        pane.update_in(cx, |pane, window, cx| {
4352            pane.close_clean_items(
4353                &CloseCleanItems {
4354                    close_pinned: false,
4355                },
4356                window,
4357                cx,
4358            )
4359        })
4360        .unwrap()
4361        .await
4362        .unwrap();
4363        assert_item_labels(&pane, ["A^", "C*^"], cx);
4364    }
4365
4366    #[gpui::test]
4367    async fn test_close_items_to_the_left(cx: &mut TestAppContext) {
4368        init_test(cx);
4369        let fs = FakeFs::new(cx.executor());
4370
4371        let project = Project::test(fs, None, cx).await;
4372        let (workspace, cx) =
4373            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4374        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
4375
4376        set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx);
4377
4378        pane.update_in(cx, |pane, window, cx| {
4379            pane.close_items_to_the_left(
4380                &CloseItemsToTheLeft {
4381                    close_pinned: false,
4382                },
4383                window,
4384                cx,
4385            )
4386        })
4387        .unwrap()
4388        .await
4389        .unwrap();
4390        assert_item_labels(&pane, ["C*", "D", "E"], cx);
4391    }
4392
4393    #[gpui::test]
4394    async fn test_close_items_to_the_right(cx: &mut TestAppContext) {
4395        init_test(cx);
4396        let fs = FakeFs::new(cx.executor());
4397
4398        let project = Project::test(fs, None, cx).await;
4399        let (workspace, cx) =
4400            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4401        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
4402
4403        set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx);
4404
4405        pane.update_in(cx, |pane, window, cx| {
4406            pane.close_items_to_the_right(
4407                &CloseItemsToTheRight {
4408                    close_pinned: false,
4409                },
4410                window,
4411                cx,
4412            )
4413        })
4414        .unwrap()
4415        .await
4416        .unwrap();
4417        assert_item_labels(&pane, ["A", "B", "C*"], cx);
4418    }
4419
4420    #[gpui::test]
4421    async fn test_close_all_items(cx: &mut TestAppContext) {
4422        init_test(cx);
4423        let fs = FakeFs::new(cx.executor());
4424
4425        let project = Project::test(fs, None, cx).await;
4426        let (workspace, cx) =
4427            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4428        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
4429
4430        let item_a = add_labeled_item(&pane, "A", false, cx);
4431        add_labeled_item(&pane, "B", false, cx);
4432        add_labeled_item(&pane, "C", false, cx);
4433        assert_item_labels(&pane, ["A", "B", "C*"], cx);
4434
4435        pane.update_in(cx, |pane, window, cx| {
4436            let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4437            pane.pin_tab_at(ix, window, cx);
4438            pane.close_all_items(
4439                &CloseAllItems {
4440                    save_intent: None,
4441                    close_pinned: false,
4442                },
4443                window,
4444                cx,
4445            )
4446        })
4447        .unwrap()
4448        .await
4449        .unwrap();
4450        assert_item_labels(&pane, ["A*"], cx);
4451
4452        pane.update_in(cx, |pane, window, cx| {
4453            let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4454            pane.unpin_tab_at(ix, window, cx);
4455            pane.close_all_items(
4456                &CloseAllItems {
4457                    save_intent: None,
4458                    close_pinned: false,
4459                },
4460                window,
4461                cx,
4462            )
4463        })
4464        .unwrap()
4465        .await
4466        .unwrap();
4467
4468        assert_item_labels(&pane, [], cx);
4469
4470        add_labeled_item(&pane, "A", true, cx).update(cx, |item, cx| {
4471            item.project_items
4472                .push(TestProjectItem::new_dirty(1, "A.txt", cx))
4473        });
4474        add_labeled_item(&pane, "B", true, cx).update(cx, |item, cx| {
4475            item.project_items
4476                .push(TestProjectItem::new_dirty(2, "B.txt", cx))
4477        });
4478        add_labeled_item(&pane, "C", true, cx).update(cx, |item, cx| {
4479            item.project_items
4480                .push(TestProjectItem::new_dirty(3, "C.txt", cx))
4481        });
4482        assert_item_labels(&pane, ["A^", "B^", "C*^"], cx);
4483
4484        let save = pane
4485            .update_in(cx, |pane, window, cx| {
4486                pane.close_all_items(
4487                    &CloseAllItems {
4488                        save_intent: None,
4489                        close_pinned: false,
4490                    },
4491                    window,
4492                    cx,
4493                )
4494            })
4495            .unwrap();
4496
4497        cx.executor().run_until_parked();
4498        cx.simulate_prompt_answer("Save all");
4499        save.await.unwrap();
4500        assert_item_labels(&pane, [], cx);
4501
4502        add_labeled_item(&pane, "A", true, cx);
4503        add_labeled_item(&pane, "B", true, cx);
4504        add_labeled_item(&pane, "C", true, cx);
4505        assert_item_labels(&pane, ["A^", "B^", "C*^"], cx);
4506        let save = pane
4507            .update_in(cx, |pane, window, cx| {
4508                pane.close_all_items(
4509                    &CloseAllItems {
4510                        save_intent: None,
4511                        close_pinned: false,
4512                    },
4513                    window,
4514                    cx,
4515                )
4516            })
4517            .unwrap();
4518
4519        cx.executor().run_until_parked();
4520        cx.simulate_prompt_answer("Discard all");
4521        save.await.unwrap();
4522        assert_item_labels(&pane, [], cx);
4523    }
4524
4525    #[gpui::test]
4526    async fn test_close_with_save_intent(cx: &mut TestAppContext) {
4527        init_test(cx);
4528        let fs = FakeFs::new(cx.executor());
4529
4530        let project = Project::test(fs, None, cx).await;
4531        let (workspace, cx) =
4532            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
4533        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
4534
4535        let a = cx.update(|_, cx| TestProjectItem::new_dirty(1, "A.txt", cx));
4536        let b = cx.update(|_, cx| TestProjectItem::new_dirty(1, "B.txt", cx));
4537        let c = cx.update(|_, cx| TestProjectItem::new_dirty(1, "C.txt", cx));
4538
4539        add_labeled_item(&pane, "AB", true, cx).update(cx, |item, _| {
4540            item.project_items.push(a.clone());
4541            item.project_items.push(b.clone());
4542        });
4543        add_labeled_item(&pane, "C", true, cx)
4544            .update(cx, |item, _| item.project_items.push(c.clone()));
4545        assert_item_labels(&pane, ["AB^", "C*^"], cx);
4546
4547        pane.update_in(cx, |pane, window, cx| {
4548            pane.close_all_items(
4549                &CloseAllItems {
4550                    save_intent: Some(SaveIntent::Save),
4551                    close_pinned: false,
4552                },
4553                window,
4554                cx,
4555            )
4556        })
4557        .unwrap()
4558        .await
4559        .unwrap();
4560
4561        assert_item_labels(&pane, [], cx);
4562        cx.update(|_, cx| {
4563            assert!(!a.read(cx).is_dirty);
4564            assert!(!b.read(cx).is_dirty);
4565            assert!(!c.read(cx).is_dirty);
4566        });
4567    }
4568
4569    #[gpui::test]
4570    async fn test_close_all_items_including_pinned(cx: &mut TestAppContext) {
4571        init_test(cx);
4572        let fs = FakeFs::new(cx.executor());
4573
4574        let project = Project::test(fs, None, cx).await;
4575        let (workspace, cx) =
4576            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
4577        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
4578
4579        let item_a = add_labeled_item(&pane, "A", false, cx);
4580        add_labeled_item(&pane, "B", false, cx);
4581        add_labeled_item(&pane, "C", false, cx);
4582        assert_item_labels(&pane, ["A", "B", "C*"], cx);
4583
4584        pane.update_in(cx, |pane, window, cx| {
4585            let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4586            pane.pin_tab_at(ix, window, cx);
4587            pane.close_all_items(
4588                &CloseAllItems {
4589                    save_intent: None,
4590                    close_pinned: true,
4591                },
4592                window,
4593                cx,
4594            )
4595        })
4596        .unwrap()
4597        .await
4598        .unwrap();
4599        assert_item_labels(&pane, [], cx);
4600    }
4601
4602    #[gpui::test]
4603    async fn test_close_pinned_tab_with_non_pinned_in_same_pane(cx: &mut TestAppContext) {
4604        init_test(cx);
4605        let fs = FakeFs::new(cx.executor());
4606        let project = Project::test(fs, None, cx).await;
4607        let (workspace, cx) =
4608            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
4609
4610        // Non-pinned tabs in same pane
4611        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
4612        add_labeled_item(&pane, "A", false, cx);
4613        add_labeled_item(&pane, "B", false, cx);
4614        add_labeled_item(&pane, "C", false, cx);
4615        pane.update_in(cx, |pane, window, cx| {
4616            pane.pin_tab_at(0, window, cx);
4617        });
4618        set_labeled_items(&pane, ["A*", "B", "C"], cx);
4619        pane.update_in(cx, |pane, window, cx| {
4620            pane.close_active_item(
4621                &CloseActiveItem {
4622                    save_intent: None,
4623                    close_pinned: false,
4624                },
4625                window,
4626                cx,
4627            );
4628        });
4629        // Non-pinned tab should be active
4630        assert_item_labels(&pane, ["A", "B*", "C"], cx);
4631    }
4632
4633    #[gpui::test]
4634    async fn test_close_pinned_tab_with_non_pinned_in_different_pane(cx: &mut TestAppContext) {
4635        init_test(cx);
4636        let fs = FakeFs::new(cx.executor());
4637        let project = Project::test(fs, None, cx).await;
4638        let (workspace, cx) =
4639            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
4640
4641        // No non-pinned tabs in same pane, non-pinned tabs in another pane
4642        let pane1 = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
4643        let pane2 = workspace.update_in(cx, |workspace, window, cx| {
4644            workspace.split_pane(pane1.clone(), SplitDirection::Right, window, cx)
4645        });
4646        add_labeled_item(&pane1, "A", false, cx);
4647        pane1.update_in(cx, |pane, window, cx| {
4648            pane.pin_tab_at(0, window, cx);
4649        });
4650        set_labeled_items(&pane1, ["A*"], cx);
4651        add_labeled_item(&pane2, "B", false, cx);
4652        set_labeled_items(&pane2, ["B"], cx);
4653        pane1.update_in(cx, |pane, window, cx| {
4654            pane.close_active_item(
4655                &CloseActiveItem {
4656                    save_intent: None,
4657                    close_pinned: false,
4658                },
4659                window,
4660                cx,
4661            );
4662        });
4663        //  Non-pinned tab of other pane should be active
4664        assert_item_labels(&pane2, ["B*"], cx);
4665    }
4666
4667    fn init_test(cx: &mut TestAppContext) {
4668        cx.update(|cx| {
4669            let settings_store = SettingsStore::test(cx);
4670            cx.set_global(settings_store);
4671            theme::init(LoadThemes::JustBase, cx);
4672            crate::init_settings(cx);
4673            Project::init_settings(cx);
4674        });
4675    }
4676
4677    fn set_max_tabs(cx: &mut TestAppContext, value: Option<usize>) {
4678        cx.update_global(|store: &mut SettingsStore, cx| {
4679            store.update_user_settings::<WorkspaceSettings>(cx, |settings| {
4680                settings.max_tabs = value.map(|v| NonZero::new(v).unwrap())
4681            });
4682        });
4683    }
4684
4685    fn add_labeled_item(
4686        pane: &Entity<Pane>,
4687        label: &str,
4688        is_dirty: bool,
4689        cx: &mut VisualTestContext,
4690    ) -> Box<Entity<TestItem>> {
4691        pane.update_in(cx, |pane, window, cx| {
4692            let labeled_item =
4693                Box::new(cx.new(|cx| TestItem::new(cx).with_label(label).with_dirty(is_dirty)));
4694            pane.add_item(labeled_item.clone(), false, false, None, window, cx);
4695            labeled_item
4696        })
4697    }
4698
4699    fn set_labeled_items<const COUNT: usize>(
4700        pane: &Entity<Pane>,
4701        labels: [&str; COUNT],
4702        cx: &mut VisualTestContext,
4703    ) -> [Box<Entity<TestItem>>; COUNT] {
4704        pane.update_in(cx, |pane, window, cx| {
4705            pane.items.clear();
4706            let mut active_item_index = 0;
4707
4708            let mut index = 0;
4709            let items = labels.map(|mut label| {
4710                if label.ends_with('*') {
4711                    label = label.trim_end_matches('*');
4712                    active_item_index = index;
4713                }
4714
4715                let labeled_item = Box::new(cx.new(|cx| TestItem::new(cx).with_label(label)));
4716                pane.add_item(labeled_item.clone(), false, false, None, window, cx);
4717                index += 1;
4718                labeled_item
4719            });
4720
4721            pane.activate_item(active_item_index, false, false, window, cx);
4722
4723            items
4724        })
4725    }
4726
4727    // Assert the item label, with the active item label suffixed with a '*'
4728    #[track_caller]
4729    fn assert_item_labels<const COUNT: usize>(
4730        pane: &Entity<Pane>,
4731        expected_states: [&str; COUNT],
4732        cx: &mut VisualTestContext,
4733    ) {
4734        let actual_states = pane.update(cx, |pane, cx| {
4735            pane.items
4736                .iter()
4737                .enumerate()
4738                .map(|(ix, item)| {
4739                    let mut state = item
4740                        .to_any()
4741                        .downcast::<TestItem>()
4742                        .unwrap()
4743                        .read(cx)
4744                        .label
4745                        .clone();
4746                    if ix == pane.active_item_index {
4747                        state.push('*');
4748                    }
4749                    if item.is_dirty(cx) {
4750                        state.push('^');
4751                    }
4752                    state
4753                })
4754                .collect::<Vec<_>>()
4755        });
4756        assert_eq!(
4757            actual_states, expected_states,
4758            "pane items do not match expectation"
4759        );
4760    }
4761}