pane.rs

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