pane.rs

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