pane.rs

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