pane.rs

   1use crate::{
   2    item::{
   3        ActivateOnClose, ClosePosition, Item, ItemHandle, ItemSettings, PreviewTabsSettings,
   4        ShowDiagnostics, TabContentParams, WeakItemHandle,
   5    },
   6    move_item,
   7    notifications::NotifyResultExt,
   8    toolbar::Toolbar,
   9    workspace_settings::{AutosaveSetting, TabBarSettings, WorkspaceSettings},
  10    CloseWindow, CopyPath, CopyRelativePath, NewFile, NewTerminal, OpenInTerminal, OpenTerminal,
  11    OpenVisible, SplitDirection, ToggleFileFinder, ToggleProjectSymbols, ToggleZoom, Workspace,
  12};
  13use anyhow::Result;
  14use collections::{BTreeSet, HashMap, HashSet, VecDeque};
  15use futures::{stream::FuturesUnordered, StreamExt};
  16use gpui::{
  17    actions, anchored, deferred, impl_actions, prelude::*, Action, 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_text(cx), |tab, text| {
2153                tab.tooltip(move |cx| Tooltip::text(text.clone(), cx))
2154            })
2155            .start_slot::<Indicator>(indicator)
2156            .map(|this| {
2157                let end_slot_action: &'static dyn Action;
2158                let end_slot_tooltip_text: &'static str;
2159                let end_slot = if is_pinned {
2160                    end_slot_action = &TogglePinTab;
2161                    end_slot_tooltip_text = "Unpin Tab";
2162                    IconButton::new("unpin tab", IconName::Pin)
2163                        .shape(IconButtonShape::Square)
2164                        .icon_color(Color::Muted)
2165                        .size(ButtonSize::None)
2166                        .icon_size(IconSize::XSmall)
2167                        .on_click(cx.listener(move |pane, _, cx| {
2168                            pane.unpin_tab_at(ix, cx);
2169                        }))
2170                } else {
2171                    end_slot_action = &CloseActiveItem { save_intent: None };
2172                    end_slot_tooltip_text = "Close Tab";
2173                    IconButton::new("close tab", IconName::Close)
2174                        .when(!always_show_close_button, |button| {
2175                            button.visible_on_hover("")
2176                        })
2177                        .shape(IconButtonShape::Square)
2178                        .icon_color(Color::Muted)
2179                        .size(ButtonSize::None)
2180                        .icon_size(IconSize::XSmall)
2181                        .on_click(cx.listener(move |pane, _, cx| {
2182                            pane.close_item_by_id(item_id, SaveIntent::Close, cx)
2183                                .detach_and_log_err(cx);
2184                        }))
2185                }
2186                .map(|this| {
2187                    if is_active {
2188                        let focus_handle = focus_handle.clone();
2189                        this.tooltip(move |cx| {
2190                            Tooltip::for_action_in(
2191                                end_slot_tooltip_text,
2192                                end_slot_action,
2193                                &focus_handle,
2194                                cx,
2195                            )
2196                        })
2197                    } else {
2198                        this.tooltip(move |cx| Tooltip::text(end_slot_tooltip_text, cx))
2199                    }
2200                });
2201                this.end_slot(end_slot)
2202            })
2203            .child(
2204                h_flex()
2205                    .gap_1()
2206                    .items_center()
2207                    .children(
2208                        std::iter::once(if let Some(decorated_icon) = decorated_icon {
2209                            Some(div().child(decorated_icon.into_any_element()))
2210                        } else if let Some(icon) = icon {
2211                            Some(div().child(icon.into_any_element()))
2212                        } else {
2213                            None
2214                        })
2215                        .flatten(),
2216                    )
2217                    .child(label),
2218            );
2219
2220        let single_entry_to_resolve = {
2221            let item_entries = self.items[ix].project_entry_ids(cx);
2222            if item_entries.len() == 1 {
2223                Some(item_entries[0])
2224            } else {
2225                None
2226            }
2227        };
2228
2229        let is_pinned = self.is_tab_pinned(ix);
2230        let pane = cx.view().downgrade();
2231        let menu_context = item.focus_handle(cx);
2232        right_click_menu(ix).trigger(tab).menu(move |cx| {
2233            let pane = pane.clone();
2234            let menu_context = menu_context.clone();
2235            ContextMenu::build(cx, move |mut menu, cx| {
2236                if let Some(pane) = pane.upgrade() {
2237                    menu = menu
2238                        .entry(
2239                            "Close",
2240                            Some(Box::new(CloseActiveItem { save_intent: None })),
2241                            cx.handler_for(&pane, move |pane, cx| {
2242                                pane.close_item_by_id(item_id, SaveIntent::Close, cx)
2243                                    .detach_and_log_err(cx);
2244                            }),
2245                        )
2246                        .entry(
2247                            "Close Others",
2248                            Some(Box::new(CloseInactiveItems {
2249                                save_intent: None,
2250                                close_pinned: false,
2251                            })),
2252                            cx.handler_for(&pane, move |pane, cx| {
2253                                pane.close_items(cx, SaveIntent::Close, |id| id != item_id)
2254                                    .detach_and_log_err(cx);
2255                            }),
2256                        )
2257                        .separator()
2258                        .entry(
2259                            "Close Left",
2260                            Some(Box::new(CloseItemsToTheLeft {
2261                                close_pinned: false,
2262                            })),
2263                            cx.handler_for(&pane, move |pane, cx| {
2264                                pane.close_items_to_the_left_by_id(
2265                                    item_id,
2266                                    &CloseItemsToTheLeft {
2267                                        close_pinned: false,
2268                                    },
2269                                    pane.get_non_closeable_item_ids(false),
2270                                    cx,
2271                                )
2272                                .detach_and_log_err(cx);
2273                            }),
2274                        )
2275                        .entry(
2276                            "Close Right",
2277                            Some(Box::new(CloseItemsToTheRight {
2278                                close_pinned: false,
2279                            })),
2280                            cx.handler_for(&pane, move |pane, cx| {
2281                                pane.close_items_to_the_right_by_id(
2282                                    item_id,
2283                                    &CloseItemsToTheRight {
2284                                        close_pinned: false,
2285                                    },
2286                                    pane.get_non_closeable_item_ids(false),
2287                                    cx,
2288                                )
2289                                .detach_and_log_err(cx);
2290                            }),
2291                        )
2292                        .separator()
2293                        .entry(
2294                            "Close Clean",
2295                            Some(Box::new(CloseCleanItems {
2296                                close_pinned: false,
2297                            })),
2298                            cx.handler_for(&pane, move |pane, cx| {
2299                                if let Some(task) = pane.close_clean_items(
2300                                    &CloseCleanItems {
2301                                        close_pinned: false,
2302                                    },
2303                                    cx,
2304                                ) {
2305                                    task.detach_and_log_err(cx)
2306                                }
2307                            }),
2308                        )
2309                        .entry(
2310                            "Close All",
2311                            Some(Box::new(CloseAllItems {
2312                                save_intent: None,
2313                                close_pinned: false,
2314                            })),
2315                            cx.handler_for(&pane, |pane, cx| {
2316                                if let Some(task) = pane.close_all_items(
2317                                    &CloseAllItems {
2318                                        save_intent: None,
2319                                        close_pinned: false,
2320                                    },
2321                                    cx,
2322                                ) {
2323                                    task.detach_and_log_err(cx)
2324                                }
2325                            }),
2326                        );
2327
2328                    let pin_tab_entries = |menu: ContextMenu| {
2329                        menu.separator().map(|this| {
2330                            if is_pinned {
2331                                this.entry(
2332                                    "Unpin Tab",
2333                                    Some(TogglePinTab.boxed_clone()),
2334                                    cx.handler_for(&pane, move |pane, cx| {
2335                                        pane.unpin_tab_at(ix, cx);
2336                                    }),
2337                                )
2338                            } else {
2339                                this.entry(
2340                                    "Pin Tab",
2341                                    Some(TogglePinTab.boxed_clone()),
2342                                    cx.handler_for(&pane, move |pane, cx| {
2343                                        pane.pin_tab_at(ix, cx);
2344                                    }),
2345                                )
2346                            }
2347                        })
2348                    };
2349                    if let Some(entry) = single_entry_to_resolve {
2350                        let entry_abs_path = pane.read(cx).entry_abs_path(entry, cx);
2351                        let parent_abs_path = entry_abs_path
2352                            .as_deref()
2353                            .and_then(|abs_path| Some(abs_path.parent()?.to_path_buf()));
2354                        let relative_path = pane
2355                            .read(cx)
2356                            .item_for_entry(entry, cx)
2357                            .and_then(|item| item.project_path(cx))
2358                            .map(|project_path| project_path.path);
2359
2360                        let entry_id = entry.to_proto();
2361                        menu = menu
2362                            .separator()
2363                            .when_some(entry_abs_path, |menu, abs_path| {
2364                                menu.entry(
2365                                    "Copy Path",
2366                                    Some(Box::new(CopyPath)),
2367                                    cx.handler_for(&pane, move |_, cx| {
2368                                        cx.write_to_clipboard(ClipboardItem::new_string(
2369                                            abs_path.to_string_lossy().to_string(),
2370                                        ));
2371                                    }),
2372                                )
2373                            })
2374                            .when_some(relative_path, |menu, relative_path| {
2375                                menu.entry(
2376                                    "Copy Relative Path",
2377                                    Some(Box::new(CopyRelativePath)),
2378                                    cx.handler_for(&pane, move |_, cx| {
2379                                        cx.write_to_clipboard(ClipboardItem::new_string(
2380                                            relative_path.to_string_lossy().to_string(),
2381                                        ));
2382                                    }),
2383                                )
2384                            })
2385                            .map(pin_tab_entries)
2386                            .separator()
2387                            .entry(
2388                                "Reveal In Project Panel",
2389                                Some(Box::new(RevealInProjectPanel {
2390                                    entry_id: Some(entry_id),
2391                                })),
2392                                cx.handler_for(&pane, move |pane, cx| {
2393                                    pane.project
2394                                        .update(cx, |_, cx| {
2395                                            cx.emit(project::Event::RevealInProjectPanel(
2396                                                ProjectEntryId::from_proto(entry_id),
2397                                            ))
2398                                        })
2399                                        .ok();
2400                                }),
2401                            )
2402                            .when_some(parent_abs_path, |menu, parent_abs_path| {
2403                                menu.entry(
2404                                    "Open in Terminal",
2405                                    Some(Box::new(OpenInTerminal)),
2406                                    cx.handler_for(&pane, move |_, cx| {
2407                                        cx.dispatch_action(
2408                                            OpenTerminal {
2409                                                working_directory: parent_abs_path.clone(),
2410                                            }
2411                                            .boxed_clone(),
2412                                        );
2413                                    }),
2414                                )
2415                            });
2416                    } else {
2417                        menu = menu.map(pin_tab_entries);
2418                    }
2419                }
2420
2421                menu.context(menu_context)
2422            })
2423        })
2424    }
2425
2426    fn render_tab_bar(&mut self, cx: &mut ViewContext<Pane>) -> impl IntoElement {
2427        let focus_handle = self.focus_handle.clone();
2428        let navigate_backward = IconButton::new("navigate_backward", IconName::ArrowLeft)
2429            .icon_size(IconSize::Small)
2430            .on_click({
2431                let view = cx.view().clone();
2432                move |_, cx| view.update(cx, Self::navigate_backward)
2433            })
2434            .disabled(!self.can_navigate_backward())
2435            .tooltip({
2436                let focus_handle = focus_handle.clone();
2437                move |cx| Tooltip::for_action_in("Go Back", &GoBack, &focus_handle, cx)
2438            });
2439
2440        let navigate_forward = IconButton::new("navigate_forward", IconName::ArrowRight)
2441            .icon_size(IconSize::Small)
2442            .on_click({
2443                let view = cx.view().clone();
2444                move |_, cx| view.update(cx, Self::navigate_forward)
2445            })
2446            .disabled(!self.can_navigate_forward())
2447            .tooltip({
2448                let focus_handle = focus_handle.clone();
2449                move |cx| Tooltip::for_action_in("Go Forward", &GoForward, &focus_handle, cx)
2450            });
2451
2452        let mut tab_items = self
2453            .items
2454            .iter()
2455            .enumerate()
2456            .zip(tab_details(&self.items, cx))
2457            .map(|((ix, item), detail)| self.render_tab(ix, &**item, detail, &focus_handle, cx))
2458            .collect::<Vec<_>>();
2459        let tab_count = tab_items.len();
2460        let unpinned_tabs = tab_items.split_off(self.pinned_tab_count);
2461        let pinned_tabs = tab_items;
2462        TabBar::new("tab_bar")
2463            .when(
2464                self.display_nav_history_buttons.unwrap_or_default(),
2465                |tab_bar| {
2466                    tab_bar
2467                        .start_child(navigate_backward)
2468                        .start_child(navigate_forward)
2469                },
2470            )
2471            .map(|tab_bar| {
2472                let render_tab_buttons = self.render_tab_bar_buttons.clone();
2473                let (left_children, right_children) = render_tab_buttons(self, cx);
2474
2475                tab_bar
2476                    .start_children(left_children)
2477                    .end_children(right_children)
2478            })
2479            .children(pinned_tabs.len().ne(&0).then(|| {
2480                h_flex()
2481                    .children(pinned_tabs)
2482                    .border_r_2()
2483                    .border_color(cx.theme().colors().border)
2484            }))
2485            .child(
2486                h_flex()
2487                    .id("unpinned tabs")
2488                    .overflow_x_scroll()
2489                    .w_full()
2490                    .track_scroll(&self.tab_bar_scroll_handle)
2491                    .children(unpinned_tabs)
2492                    .child(
2493                        div()
2494                            .id("tab_bar_drop_target")
2495                            .min_w_6()
2496                            // HACK: This empty child is currently necessary to force the drop target to appear
2497                            // despite us setting a min width above.
2498                            .child("")
2499                            .h_full()
2500                            .flex_grow()
2501                            .drag_over::<DraggedTab>(|bar, _, cx| {
2502                                bar.bg(cx.theme().colors().drop_target_background)
2503                            })
2504                            .drag_over::<DraggedSelection>(|bar, _, cx| {
2505                                bar.bg(cx.theme().colors().drop_target_background)
2506                            })
2507                            .on_drop(cx.listener(move |this, dragged_tab: &DraggedTab, cx| {
2508                                this.drag_split_direction = None;
2509                                this.handle_tab_drop(dragged_tab, this.items.len(), cx)
2510                            }))
2511                            .on_drop(cx.listener(move |this, selection: &DraggedSelection, cx| {
2512                                this.drag_split_direction = None;
2513                                this.handle_project_entry_drop(
2514                                    &selection.active_selection.entry_id,
2515                                    Some(tab_count),
2516                                    cx,
2517                                )
2518                            }))
2519                            .on_drop(cx.listener(move |this, paths, cx| {
2520                                this.drag_split_direction = None;
2521                                this.handle_external_paths_drop(paths, cx)
2522                            }))
2523                            .on_click(cx.listener(move |this, event: &ClickEvent, cx| {
2524                                if event.up.click_count == 2 {
2525                                    cx.dispatch_action(
2526                                        this.double_click_dispatch_action.boxed_clone(),
2527                                    )
2528                                }
2529                            })),
2530                    ),
2531            )
2532    }
2533
2534    pub fn render_menu_overlay(menu: &View<ContextMenu>) -> Div {
2535        div().absolute().bottom_0().right_0().size_0().child(
2536            deferred(anchored().anchor(Corner::TopRight).child(menu.clone())).with_priority(1),
2537        )
2538    }
2539
2540    pub fn set_zoomed(&mut self, zoomed: bool, cx: &mut ViewContext<Self>) {
2541        self.zoomed = zoomed;
2542        cx.notify();
2543    }
2544
2545    pub fn is_zoomed(&self) -> bool {
2546        self.zoomed
2547    }
2548
2549    fn handle_drag_move<T: 'static>(
2550        &mut self,
2551        event: &DragMoveEvent<T>,
2552        cx: &mut ViewContext<Self>,
2553    ) {
2554        let can_split_predicate = self.can_split_predicate.take();
2555        let can_split = match &can_split_predicate {
2556            Some(can_split_predicate) => can_split_predicate(self, event.dragged_item(), cx),
2557            None => false,
2558        };
2559        self.can_split_predicate = can_split_predicate;
2560        if !can_split {
2561            return;
2562        }
2563
2564        let rect = event.bounds.size;
2565
2566        let size = event.bounds.size.width.min(event.bounds.size.height)
2567            * WorkspaceSettings::get_global(cx).drop_target_size;
2568
2569        let relative_cursor = Point::new(
2570            event.event.position.x - event.bounds.left(),
2571            event.event.position.y - event.bounds.top(),
2572        );
2573
2574        let direction = if relative_cursor.x < size
2575            || relative_cursor.x > rect.width - size
2576            || relative_cursor.y < size
2577            || relative_cursor.y > rect.height - size
2578        {
2579            [
2580                SplitDirection::Up,
2581                SplitDirection::Right,
2582                SplitDirection::Down,
2583                SplitDirection::Left,
2584            ]
2585            .iter()
2586            .min_by_key(|side| match side {
2587                SplitDirection::Up => relative_cursor.y,
2588                SplitDirection::Right => rect.width - relative_cursor.x,
2589                SplitDirection::Down => rect.height - relative_cursor.y,
2590                SplitDirection::Left => relative_cursor.x,
2591            })
2592            .cloned()
2593        } else {
2594            None
2595        };
2596
2597        if direction != self.drag_split_direction {
2598            self.drag_split_direction = direction;
2599        }
2600    }
2601
2602    fn handle_tab_drop(&mut self, dragged_tab: &DraggedTab, ix: usize, cx: &mut ViewContext<Self>) {
2603        if let Some(custom_drop_handle) = self.custom_drop_handle.clone() {
2604            if let ControlFlow::Break(()) = custom_drop_handle(self, dragged_tab, cx) {
2605                return;
2606            }
2607        }
2608        let mut to_pane = cx.view().clone();
2609        let split_direction = self.drag_split_direction;
2610        let item_id = dragged_tab.item.item_id();
2611        if let Some(preview_item_id) = self.preview_item_id {
2612            if item_id == preview_item_id {
2613                self.set_preview_item_id(None, cx);
2614            }
2615        }
2616
2617        let from_pane = dragged_tab.pane.clone();
2618        self.workspace
2619            .update(cx, |_, cx| {
2620                cx.defer(move |workspace, cx| {
2621                    if let Some(split_direction) = split_direction {
2622                        to_pane = workspace.split_pane(to_pane, split_direction, cx);
2623                    }
2624                    let old_ix = from_pane.read(cx).index_for_item_id(item_id);
2625                    let old_len = to_pane.read(cx).items.len();
2626                    move_item(&from_pane, &to_pane, item_id, ix, cx);
2627                    if to_pane == from_pane {
2628                        if let Some(old_index) = old_ix {
2629                            to_pane.update(cx, |this, _| {
2630                                if old_index < this.pinned_tab_count
2631                                    && (ix == this.items.len() || ix > this.pinned_tab_count)
2632                                {
2633                                    this.pinned_tab_count -= 1;
2634                                } else if this.has_pinned_tabs()
2635                                    && old_index >= this.pinned_tab_count
2636                                    && ix < this.pinned_tab_count
2637                                {
2638                                    this.pinned_tab_count += 1;
2639                                }
2640                            });
2641                        }
2642                    } else {
2643                        to_pane.update(cx, |this, _| {
2644                            if this.items.len() > old_len // Did we not deduplicate on drag?
2645                                && this.has_pinned_tabs()
2646                                && ix < this.pinned_tab_count
2647                            {
2648                                this.pinned_tab_count += 1;
2649                            }
2650                        });
2651                        from_pane.update(cx, |this, _| {
2652                            if let Some(index) = old_ix {
2653                                if this.pinned_tab_count > index {
2654                                    this.pinned_tab_count -= 1;
2655                                }
2656                            }
2657                        })
2658                    }
2659                });
2660            })
2661            .log_err();
2662    }
2663
2664    fn handle_dragged_selection_drop(
2665        &mut self,
2666        dragged_selection: &DraggedSelection,
2667        dragged_onto: Option<usize>,
2668        cx: &mut ViewContext<Self>,
2669    ) {
2670        if let Some(custom_drop_handle) = self.custom_drop_handle.clone() {
2671            if let ControlFlow::Break(()) = custom_drop_handle(self, dragged_selection, cx) {
2672                return;
2673            }
2674        }
2675        self.handle_project_entry_drop(
2676            &dragged_selection.active_selection.entry_id,
2677            dragged_onto,
2678            cx,
2679        );
2680    }
2681
2682    fn handle_project_entry_drop(
2683        &mut self,
2684        project_entry_id: &ProjectEntryId,
2685        target: Option<usize>,
2686        cx: &mut ViewContext<Self>,
2687    ) {
2688        if let Some(custom_drop_handle) = self.custom_drop_handle.clone() {
2689            if let ControlFlow::Break(()) = custom_drop_handle(self, project_entry_id, cx) {
2690                return;
2691            }
2692        }
2693        let mut to_pane = cx.view().clone();
2694        let split_direction = self.drag_split_direction;
2695        let project_entry_id = *project_entry_id;
2696        self.workspace
2697            .update(cx, |_, cx| {
2698                cx.defer(move |workspace, cx| {
2699                    if let Some(path) = workspace
2700                        .project()
2701                        .read(cx)
2702                        .path_for_entry(project_entry_id, cx)
2703                    {
2704                        let load_path_task = workspace.load_path(path, cx);
2705                        cx.spawn(|workspace, mut cx| async move {
2706                            if let Some((project_entry_id, build_item)) =
2707                                load_path_task.await.notify_async_err(&mut cx)
2708                            {
2709                                let (to_pane, new_item_handle) = workspace
2710                                    .update(&mut cx, |workspace, cx| {
2711                                        if let Some(split_direction) = split_direction {
2712                                            to_pane =
2713                                                workspace.split_pane(to_pane, split_direction, cx);
2714                                        }
2715                                        let new_item_handle = to_pane.update(cx, |pane, cx| {
2716                                            pane.open_item(
2717                                                project_entry_id,
2718                                                true,
2719                                                false,
2720                                                target,
2721                                                cx,
2722                                                build_item,
2723                                            )
2724                                        });
2725                                        (to_pane, new_item_handle)
2726                                    })
2727                                    .log_err()?;
2728                                to_pane
2729                                    .update(&mut cx, |this, cx| {
2730                                        let Some(index) = this.index_for_item(&*new_item_handle)
2731                                        else {
2732                                            return;
2733                                        };
2734
2735                                        if target.map_or(false, |target| this.is_tab_pinned(target))
2736                                        {
2737                                            this.pin_tab_at(index, cx);
2738                                        }
2739                                    })
2740                                    .ok()?
2741                            }
2742                            Some(())
2743                        })
2744                        .detach();
2745                    };
2746                });
2747            })
2748            .log_err();
2749    }
2750
2751    fn handle_external_paths_drop(&mut self, paths: &ExternalPaths, cx: &mut ViewContext<Self>) {
2752        if let Some(custom_drop_handle) = self.custom_drop_handle.clone() {
2753            if let ControlFlow::Break(()) = custom_drop_handle(self, paths, cx) {
2754                return;
2755            }
2756        }
2757        let mut to_pane = cx.view().clone();
2758        let mut split_direction = self.drag_split_direction;
2759        let paths = paths.paths().to_vec();
2760        let is_remote = self
2761            .workspace
2762            .update(cx, |workspace, cx| {
2763                if workspace.project().read(cx).is_via_collab() {
2764                    workspace.show_error(
2765                        &anyhow::anyhow!("Cannot drop files on a remote project"),
2766                        cx,
2767                    );
2768                    true
2769                } else {
2770                    false
2771                }
2772            })
2773            .unwrap_or(true);
2774        if is_remote {
2775            return;
2776        }
2777
2778        self.workspace
2779            .update(cx, |workspace, cx| {
2780                let fs = Arc::clone(workspace.project().read(cx).fs());
2781                cx.spawn(|workspace, mut cx| async move {
2782                    let mut is_file_checks = FuturesUnordered::new();
2783                    for path in &paths {
2784                        is_file_checks.push(fs.is_file(path))
2785                    }
2786                    let mut has_files_to_open = false;
2787                    while let Some(is_file) = is_file_checks.next().await {
2788                        if is_file {
2789                            has_files_to_open = true;
2790                            break;
2791                        }
2792                    }
2793                    drop(is_file_checks);
2794                    if !has_files_to_open {
2795                        split_direction = None;
2796                    }
2797
2798                    if let Ok(open_task) = workspace.update(&mut cx, |workspace, cx| {
2799                        if let Some(split_direction) = split_direction {
2800                            to_pane = workspace.split_pane(to_pane, split_direction, cx);
2801                        }
2802                        workspace.open_paths(
2803                            paths,
2804                            OpenVisible::OnlyDirectories,
2805                            Some(to_pane.downgrade()),
2806                            cx,
2807                        )
2808                    }) {
2809                        let opened_items: Vec<_> = open_task.await;
2810                        _ = workspace.update(&mut cx, |workspace, cx| {
2811                            for item in opened_items.into_iter().flatten() {
2812                                if let Err(e) = item {
2813                                    workspace.show_error(&e, cx);
2814                                }
2815                            }
2816                        });
2817                    }
2818                })
2819                .detach();
2820            })
2821            .log_err();
2822    }
2823
2824    pub fn display_nav_history_buttons(&mut self, display: Option<bool>) {
2825        self.display_nav_history_buttons = display;
2826    }
2827
2828    fn get_non_closeable_item_ids(&self, close_pinned: bool) -> Vec<EntityId> {
2829        if close_pinned {
2830            return vec![];
2831        }
2832
2833        self.items
2834            .iter()
2835            .map(|item| item.item_id())
2836            .filter(|item_id| {
2837                if let Some(ix) = self.index_for_item_id(*item_id) {
2838                    self.is_tab_pinned(ix)
2839                } else {
2840                    true
2841                }
2842            })
2843            .collect()
2844    }
2845
2846    pub fn drag_split_direction(&self) -> Option<SplitDirection> {
2847        self.drag_split_direction
2848    }
2849
2850    pub fn set_zoom_out_on_close(&mut self, zoom_out_on_close: bool) {
2851        self.zoom_out_on_close = zoom_out_on_close;
2852    }
2853}
2854
2855impl FocusableView for Pane {
2856    fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
2857        self.focus_handle.clone()
2858    }
2859}
2860
2861impl Render for Pane {
2862    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
2863        let mut key_context = KeyContext::new_with_defaults();
2864        key_context.add("Pane");
2865        if self.active_item().is_none() {
2866            key_context.add("EmptyPane");
2867        }
2868
2869        let should_display_tab_bar = self.should_display_tab_bar.clone();
2870        let display_tab_bar = should_display_tab_bar(cx);
2871        let Some(project) = self.project.upgrade() else {
2872            return div().track_focus(&self.focus_handle(cx));
2873        };
2874        let is_local = project.read(cx).is_local();
2875
2876        v_flex()
2877            .key_context(key_context)
2878            .track_focus(&self.focus_handle(cx))
2879            .size_full()
2880            .flex_none()
2881            .overflow_hidden()
2882            .on_action(cx.listener(|pane, _: &AlternateFile, cx| {
2883                pane.alternate_file(cx);
2884            }))
2885            .on_action(cx.listener(|pane, _: &SplitLeft, cx| pane.split(SplitDirection::Left, cx)))
2886            .on_action(cx.listener(|pane, _: &SplitUp, cx| pane.split(SplitDirection::Up, cx)))
2887            .on_action(cx.listener(|pane, _: &SplitHorizontal, cx| {
2888                pane.split(SplitDirection::horizontal(cx), cx)
2889            }))
2890            .on_action(cx.listener(|pane, _: &SplitVertical, cx| {
2891                pane.split(SplitDirection::vertical(cx), cx)
2892            }))
2893            .on_action(
2894                cx.listener(|pane, _: &SplitRight, cx| pane.split(SplitDirection::Right, cx)),
2895            )
2896            .on_action(cx.listener(|pane, _: &SplitDown, cx| pane.split(SplitDirection::Down, cx)))
2897            .on_action(cx.listener(|pane, _: &GoBack, cx| pane.navigate_backward(cx)))
2898            .on_action(cx.listener(|pane, _: &GoForward, cx| pane.navigate_forward(cx)))
2899            .on_action(cx.listener(|pane, _: &JoinIntoNext, cx| pane.join_into_next(cx)))
2900            .on_action(cx.listener(|pane, _: &JoinAll, cx| pane.join_all(cx)))
2901            .on_action(cx.listener(Pane::toggle_zoom))
2902            .on_action(cx.listener(|pane: &mut Pane, action: &ActivateItem, cx| {
2903                pane.activate_item(action.0, true, true, cx);
2904            }))
2905            .on_action(cx.listener(|pane: &mut Pane, _: &ActivateLastItem, cx| {
2906                pane.activate_item(pane.items.len() - 1, true, true, cx);
2907            }))
2908            .on_action(cx.listener(|pane: &mut Pane, _: &ActivatePrevItem, cx| {
2909                pane.activate_prev_item(true, cx);
2910            }))
2911            .on_action(cx.listener(|pane: &mut Pane, _: &ActivateNextItem, cx| {
2912                pane.activate_next_item(true, cx);
2913            }))
2914            .on_action(cx.listener(|pane, _: &SwapItemLeft, cx| pane.swap_item_left(cx)))
2915            .on_action(cx.listener(|pane, _: &SwapItemRight, cx| pane.swap_item_right(cx)))
2916            .on_action(cx.listener(|pane, action, cx| {
2917                pane.toggle_pin_tab(action, cx);
2918            }))
2919            .when(PreviewTabsSettings::get_global(cx).enabled, |this| {
2920                this.on_action(cx.listener(|pane: &mut Pane, _: &TogglePreviewTab, cx| {
2921                    if let Some(active_item_id) = pane.active_item().map(|i| i.item_id()) {
2922                        if pane.is_active_preview_item(active_item_id) {
2923                            pane.set_preview_item_id(None, cx);
2924                        } else {
2925                            pane.set_preview_item_id(Some(active_item_id), cx);
2926                        }
2927                    }
2928                }))
2929            })
2930            .on_action(
2931                cx.listener(|pane: &mut Self, action: &CloseActiveItem, cx| {
2932                    if let Some(task) = pane.close_active_item(action, cx) {
2933                        task.detach_and_log_err(cx)
2934                    }
2935                }),
2936            )
2937            .on_action(
2938                cx.listener(|pane: &mut Self, action: &CloseInactiveItems, cx| {
2939                    if let Some(task) = pane.close_inactive_items(action, cx) {
2940                        task.detach_and_log_err(cx)
2941                    }
2942                }),
2943            )
2944            .on_action(
2945                cx.listener(|pane: &mut Self, action: &CloseCleanItems, cx| {
2946                    if let Some(task) = pane.close_clean_items(action, cx) {
2947                        task.detach_and_log_err(cx)
2948                    }
2949                }),
2950            )
2951            .on_action(
2952                cx.listener(|pane: &mut Self, action: &CloseItemsToTheLeft, cx| {
2953                    if let Some(task) = pane.close_items_to_the_left(action, cx) {
2954                        task.detach_and_log_err(cx)
2955                    }
2956                }),
2957            )
2958            .on_action(
2959                cx.listener(|pane: &mut Self, action: &CloseItemsToTheRight, cx| {
2960                    if let Some(task) = pane.close_items_to_the_right(action, cx) {
2961                        task.detach_and_log_err(cx)
2962                    }
2963                }),
2964            )
2965            .on_action(cx.listener(|pane: &mut Self, action: &CloseAllItems, cx| {
2966                if let Some(task) = pane.close_all_items(action, cx) {
2967                    task.detach_and_log_err(cx)
2968                }
2969            }))
2970            .on_action(
2971                cx.listener(|pane: &mut Self, action: &CloseActiveItem, cx| {
2972                    if let Some(task) = pane.close_active_item(action, cx) {
2973                        task.detach_and_log_err(cx)
2974                    }
2975                }),
2976            )
2977            .on_action(
2978                cx.listener(|pane: &mut Self, action: &RevealInProjectPanel, cx| {
2979                    let entry_id = action
2980                        .entry_id
2981                        .map(ProjectEntryId::from_proto)
2982                        .or_else(|| pane.active_item()?.project_entry_ids(cx).first().copied());
2983                    if let Some(entry_id) = entry_id {
2984                        pane.project
2985                            .update(cx, |_, cx| {
2986                                cx.emit(project::Event::RevealInProjectPanel(entry_id))
2987                            })
2988                            .ok();
2989                    }
2990                }),
2991            )
2992            .when(self.active_item().is_some() && display_tab_bar, |pane| {
2993                pane.child(self.render_tab_bar(cx))
2994            })
2995            .child({
2996                let has_worktrees = project.read(cx).worktrees(cx).next().is_some();
2997                // main content
2998                div()
2999                    .flex_1()
3000                    .relative()
3001                    .group("")
3002                    .overflow_hidden()
3003                    .on_drag_move::<DraggedTab>(cx.listener(Self::handle_drag_move))
3004                    .on_drag_move::<DraggedSelection>(cx.listener(Self::handle_drag_move))
3005                    .when(is_local, |div| {
3006                        div.on_drag_move::<ExternalPaths>(cx.listener(Self::handle_drag_move))
3007                    })
3008                    .map(|div| {
3009                        if let Some(item) = self.active_item() {
3010                            div.v_flex()
3011                                .size_full()
3012                                .overflow_hidden()
3013                                .child(self.toolbar.clone())
3014                                .child(item.to_any())
3015                        } else {
3016                            let placeholder = div.h_flex().size_full().justify_center();
3017                            if has_worktrees {
3018                                placeholder
3019                            } else {
3020                                placeholder.child(
3021                                    Label::new("Open a file or project to get started.")
3022                                        .color(Color::Muted),
3023                                )
3024                            }
3025                        }
3026                    })
3027                    .child(
3028                        // drag target
3029                        div()
3030                            .invisible()
3031                            .absolute()
3032                            .bg(cx.theme().colors().drop_target_background)
3033                            .group_drag_over::<DraggedTab>("", |style| style.visible())
3034                            .group_drag_over::<DraggedSelection>("", |style| style.visible())
3035                            .when(is_local, |div| {
3036                                div.group_drag_over::<ExternalPaths>("", |style| style.visible())
3037                            })
3038                            .when_some(self.can_drop_predicate.clone(), |this, p| {
3039                                this.can_drop(move |a, cx| p(a, cx))
3040                            })
3041                            .on_drop(cx.listener(move |this, dragged_tab, cx| {
3042                                this.handle_tab_drop(dragged_tab, this.active_item_index(), cx)
3043                            }))
3044                            .on_drop(cx.listener(move |this, selection: &DraggedSelection, cx| {
3045                                this.handle_dragged_selection_drop(selection, None, cx)
3046                            }))
3047                            .on_drop(cx.listener(move |this, paths, cx| {
3048                                this.handle_external_paths_drop(paths, cx)
3049                            }))
3050                            .map(|div| {
3051                                let size = DefiniteLength::Fraction(0.5);
3052                                match self.drag_split_direction {
3053                                    None => div.top_0().right_0().bottom_0().left_0(),
3054                                    Some(SplitDirection::Up) => {
3055                                        div.top_0().left_0().right_0().h(size)
3056                                    }
3057                                    Some(SplitDirection::Down) => {
3058                                        div.left_0().bottom_0().right_0().h(size)
3059                                    }
3060                                    Some(SplitDirection::Left) => {
3061                                        div.top_0().left_0().bottom_0().w(size)
3062                                    }
3063                                    Some(SplitDirection::Right) => {
3064                                        div.top_0().bottom_0().right_0().w(size)
3065                                    }
3066                                }
3067                            }),
3068                    )
3069            })
3070            .on_mouse_down(
3071                MouseButton::Navigate(NavigationDirection::Back),
3072                cx.listener(|pane, _, cx| {
3073                    if let Some(workspace) = pane.workspace.upgrade() {
3074                        let pane = cx.view().downgrade();
3075                        cx.window_context().defer(move |cx| {
3076                            workspace.update(cx, |workspace, cx| {
3077                                workspace.go_back(pane, cx).detach_and_log_err(cx)
3078                            })
3079                        })
3080                    }
3081                }),
3082            )
3083            .on_mouse_down(
3084                MouseButton::Navigate(NavigationDirection::Forward),
3085                cx.listener(|pane, _, cx| {
3086                    if let Some(workspace) = pane.workspace.upgrade() {
3087                        let pane = cx.view().downgrade();
3088                        cx.window_context().defer(move |cx| {
3089                            workspace.update(cx, |workspace, cx| {
3090                                workspace.go_forward(pane, cx).detach_and_log_err(cx)
3091                            })
3092                        })
3093                    }
3094                }),
3095            )
3096    }
3097}
3098
3099impl ItemNavHistory {
3100    pub fn push<D: 'static + Send + Any>(&mut self, data: Option<D>, cx: &mut WindowContext) {
3101        if self
3102            .item
3103            .upgrade()
3104            .is_some_and(|item| item.include_in_nav_history())
3105        {
3106            self.history
3107                .push(data, self.item.clone(), self.is_preview, cx);
3108        }
3109    }
3110
3111    pub fn pop_backward(&mut self, cx: &mut WindowContext) -> Option<NavigationEntry> {
3112        self.history.pop(NavigationMode::GoingBack, cx)
3113    }
3114
3115    pub fn pop_forward(&mut self, cx: &mut WindowContext) -> Option<NavigationEntry> {
3116        self.history.pop(NavigationMode::GoingForward, cx)
3117    }
3118}
3119
3120impl NavHistory {
3121    pub fn for_each_entry(
3122        &self,
3123        cx: &AppContext,
3124        mut f: impl FnMut(&NavigationEntry, (ProjectPath, Option<PathBuf>)),
3125    ) {
3126        let borrowed_history = self.0.lock();
3127        borrowed_history
3128            .forward_stack
3129            .iter()
3130            .chain(borrowed_history.backward_stack.iter())
3131            .chain(borrowed_history.closed_stack.iter())
3132            .for_each(|entry| {
3133                if let Some(project_and_abs_path) =
3134                    borrowed_history.paths_by_item.get(&entry.item.id())
3135                {
3136                    f(entry, project_and_abs_path.clone());
3137                } else if let Some(item) = entry.item.upgrade() {
3138                    if let Some(path) = item.project_path(cx) {
3139                        f(entry, (path, None));
3140                    }
3141                }
3142            })
3143    }
3144
3145    pub fn set_mode(&mut self, mode: NavigationMode) {
3146        self.0.lock().mode = mode;
3147    }
3148
3149    pub fn mode(&self) -> NavigationMode {
3150        self.0.lock().mode
3151    }
3152
3153    pub fn disable(&mut self) {
3154        self.0.lock().mode = NavigationMode::Disabled;
3155    }
3156
3157    pub fn enable(&mut self) {
3158        self.0.lock().mode = NavigationMode::Normal;
3159    }
3160
3161    pub fn pop(&mut self, mode: NavigationMode, cx: &mut WindowContext) -> Option<NavigationEntry> {
3162        let mut state = self.0.lock();
3163        let entry = match mode {
3164            NavigationMode::Normal | NavigationMode::Disabled | NavigationMode::ClosingItem => {
3165                return None
3166            }
3167            NavigationMode::GoingBack => &mut state.backward_stack,
3168            NavigationMode::GoingForward => &mut state.forward_stack,
3169            NavigationMode::ReopeningClosedItem => &mut state.closed_stack,
3170        }
3171        .pop_back();
3172        if entry.is_some() {
3173            state.did_update(cx);
3174        }
3175        entry
3176    }
3177
3178    pub fn push<D: 'static + Send + Any>(
3179        &mut self,
3180        data: Option<D>,
3181        item: Arc<dyn WeakItemHandle>,
3182        is_preview: bool,
3183        cx: &mut WindowContext,
3184    ) {
3185        let state = &mut *self.0.lock();
3186        match state.mode {
3187            NavigationMode::Disabled => {}
3188            NavigationMode::Normal | NavigationMode::ReopeningClosedItem => {
3189                if state.backward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
3190                    state.backward_stack.pop_front();
3191                }
3192                state.backward_stack.push_back(NavigationEntry {
3193                    item,
3194                    data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
3195                    timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
3196                    is_preview,
3197                });
3198                state.forward_stack.clear();
3199            }
3200            NavigationMode::GoingBack => {
3201                if state.forward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
3202                    state.forward_stack.pop_front();
3203                }
3204                state.forward_stack.push_back(NavigationEntry {
3205                    item,
3206                    data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
3207                    timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
3208                    is_preview,
3209                });
3210            }
3211            NavigationMode::GoingForward => {
3212                if state.backward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
3213                    state.backward_stack.pop_front();
3214                }
3215                state.backward_stack.push_back(NavigationEntry {
3216                    item,
3217                    data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
3218                    timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
3219                    is_preview,
3220                });
3221            }
3222            NavigationMode::ClosingItem => {
3223                if state.closed_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
3224                    state.closed_stack.pop_front();
3225                }
3226                state.closed_stack.push_back(NavigationEntry {
3227                    item,
3228                    data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
3229                    timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
3230                    is_preview,
3231                });
3232            }
3233        }
3234        state.did_update(cx);
3235    }
3236
3237    pub fn remove_item(&mut self, item_id: EntityId) {
3238        let mut state = self.0.lock();
3239        state.paths_by_item.remove(&item_id);
3240        state
3241            .backward_stack
3242            .retain(|entry| entry.item.id() != item_id);
3243        state
3244            .forward_stack
3245            .retain(|entry| entry.item.id() != item_id);
3246        state
3247            .closed_stack
3248            .retain(|entry| entry.item.id() != item_id);
3249    }
3250
3251    pub fn path_for_item(&self, item_id: EntityId) -> Option<(ProjectPath, Option<PathBuf>)> {
3252        self.0.lock().paths_by_item.get(&item_id).cloned()
3253    }
3254}
3255
3256impl NavHistoryState {
3257    pub fn did_update(&self, cx: &mut WindowContext) {
3258        if let Some(pane) = self.pane.upgrade() {
3259            cx.defer(move |cx| {
3260                pane.update(cx, |pane, cx| pane.history_updated(cx));
3261            });
3262        }
3263    }
3264}
3265
3266fn dirty_message_for(buffer_path: Option<ProjectPath>) -> String {
3267    let path = buffer_path
3268        .as_ref()
3269        .and_then(|p| {
3270            p.path
3271                .to_str()
3272                .and_then(|s| if s.is_empty() { None } else { Some(s) })
3273        })
3274        .unwrap_or("This buffer");
3275    let path = truncate_and_remove_front(path, 80);
3276    format!("{path} contains unsaved edits. Do you want to save it?")
3277}
3278
3279pub fn tab_details(items: &[Box<dyn ItemHandle>], cx: &AppContext) -> Vec<usize> {
3280    let mut tab_details = items.iter().map(|_| 0).collect::<Vec<_>>();
3281    let mut tab_descriptions = HashMap::default();
3282    let mut done = false;
3283    while !done {
3284        done = true;
3285
3286        // Store item indices by their tab description.
3287        for (ix, (item, detail)) in items.iter().zip(&tab_details).enumerate() {
3288            if let Some(description) = item.tab_description(*detail, cx) {
3289                if *detail == 0
3290                    || Some(&description) != item.tab_description(detail - 1, cx).as_ref()
3291                {
3292                    tab_descriptions
3293                        .entry(description)
3294                        .or_insert(Vec::new())
3295                        .push(ix);
3296                }
3297            }
3298        }
3299
3300        // If two or more items have the same tab description, increase their level
3301        // of detail and try again.
3302        for (_, item_ixs) in tab_descriptions.drain() {
3303            if item_ixs.len() > 1 {
3304                done = false;
3305                for ix in item_ixs {
3306                    tab_details[ix] += 1;
3307                }
3308            }
3309        }
3310    }
3311
3312    tab_details
3313}
3314
3315pub fn render_item_indicator(item: Box<dyn ItemHandle>, cx: &WindowContext) -> Option<Indicator> {
3316    maybe!({
3317        let indicator_color = match (item.has_conflict(cx), item.is_dirty(cx)) {
3318            (true, _) => Color::Warning,
3319            (_, true) => Color::Accent,
3320            (false, false) => return None,
3321        };
3322
3323        Some(Indicator::dot().color(indicator_color))
3324    })
3325}
3326
3327impl Render for DraggedTab {
3328    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
3329        let ui_font = ThemeSettings::get_global(cx).ui_font.clone();
3330        let label = self.item.tab_content(
3331            TabContentParams {
3332                detail: Some(self.detail),
3333                selected: false,
3334                preview: false,
3335            },
3336            cx,
3337        );
3338        Tab::new("")
3339            .toggle_state(self.is_active)
3340            .child(label)
3341            .render(cx)
3342            .font(ui_font)
3343    }
3344}
3345
3346#[cfg(test)]
3347mod tests {
3348    use std::num::NonZero;
3349
3350    use super::*;
3351    use crate::item::test::{TestItem, TestProjectItem};
3352    use gpui::{TestAppContext, VisualTestContext};
3353    use project::FakeFs;
3354    use settings::SettingsStore;
3355    use theme::LoadThemes;
3356
3357    #[gpui::test]
3358    async fn test_remove_active_empty(cx: &mut TestAppContext) {
3359        init_test(cx);
3360        let fs = FakeFs::new(cx.executor());
3361
3362        let project = Project::test(fs, None, cx).await;
3363        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
3364        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3365
3366        pane.update(cx, |pane, cx| {
3367            assert!(pane
3368                .close_active_item(&CloseActiveItem { save_intent: None }, cx)
3369                .is_none())
3370        });
3371    }
3372
3373    #[gpui::test]
3374    async fn test_add_item_capped_to_max_tabs(cx: &mut TestAppContext) {
3375        init_test(cx);
3376        let fs = FakeFs::new(cx.executor());
3377
3378        let project = Project::test(fs, None, cx).await;
3379        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
3380        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3381
3382        for i in 0..7 {
3383            add_labeled_item(&pane, format!("{}", i).as_str(), false, cx);
3384        }
3385        set_max_tabs(cx, Some(5));
3386        add_labeled_item(&pane, "7", false, cx);
3387        // Remove items to respect the max tab cap.
3388        assert_item_labels(&pane, ["3", "4", "5", "6", "7*"], cx);
3389        pane.update(cx, |pane, cx| {
3390            pane.activate_item(0, false, false, cx);
3391        });
3392        add_labeled_item(&pane, "X", false, cx);
3393        // Respect activation order.
3394        assert_item_labels(&pane, ["3", "X*", "5", "6", "7"], cx);
3395
3396        for i in 0..7 {
3397            add_labeled_item(&pane, format!("D{}", i).as_str(), true, cx);
3398        }
3399        // Keeps dirty items, even over max tab cap.
3400        assert_item_labels(
3401            &pane,
3402            ["D0^", "D1^", "D2^", "D3^", "D4^", "D5^", "D6*^"],
3403            cx,
3404        );
3405
3406        set_max_tabs(cx, None);
3407        for i in 0..7 {
3408            add_labeled_item(&pane, format!("N{}", i).as_str(), false, cx);
3409        }
3410        // No cap when max tabs is None.
3411        assert_item_labels(
3412            &pane,
3413            [
3414                "D0^", "D1^", "D2^", "D3^", "D4^", "D5^", "D6^", "N0", "N1", "N2", "N3", "N4",
3415                "N5", "N6*",
3416            ],
3417            cx,
3418        );
3419    }
3420
3421    #[gpui::test]
3422    async fn test_add_item_with_new_item(cx: &mut TestAppContext) {
3423        init_test(cx);
3424        let fs = FakeFs::new(cx.executor());
3425
3426        let project = Project::test(fs, None, cx).await;
3427        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
3428        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3429
3430        // 1. Add with a destination index
3431        //   a. Add before the active item
3432        set_labeled_items(&pane, ["A", "B*", "C"], cx);
3433        pane.update(cx, |pane, cx| {
3434            pane.add_item(
3435                Box::new(cx.new_view(|cx| TestItem::new(cx).with_label("D"))),
3436                false,
3437                false,
3438                Some(0),
3439                cx,
3440            );
3441        });
3442        assert_item_labels(&pane, ["D*", "A", "B", "C"], cx);
3443
3444        //   b. Add after the active item
3445        set_labeled_items(&pane, ["A", "B*", "C"], cx);
3446        pane.update(cx, |pane, cx| {
3447            pane.add_item(
3448                Box::new(cx.new_view(|cx| TestItem::new(cx).with_label("D"))),
3449                false,
3450                false,
3451                Some(2),
3452                cx,
3453            );
3454        });
3455        assert_item_labels(&pane, ["A", "B", "D*", "C"], cx);
3456
3457        //   c. Add at the end of the item list (including off the length)
3458        set_labeled_items(&pane, ["A", "B*", "C"], cx);
3459        pane.update(cx, |pane, cx| {
3460            pane.add_item(
3461                Box::new(cx.new_view(|cx| TestItem::new(cx).with_label("D"))),
3462                false,
3463                false,
3464                Some(5),
3465                cx,
3466            );
3467        });
3468        assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
3469
3470        // 2. Add without a destination index
3471        //   a. Add with active item at the start of the item list
3472        set_labeled_items(&pane, ["A*", "B", "C"], cx);
3473        pane.update(cx, |pane, cx| {
3474            pane.add_item(
3475                Box::new(cx.new_view(|cx| TestItem::new(cx).with_label("D"))),
3476                false,
3477                false,
3478                None,
3479                cx,
3480            );
3481        });
3482        set_labeled_items(&pane, ["A", "D*", "B", "C"], cx);
3483
3484        //   b. Add with active item at the end of the item list
3485        set_labeled_items(&pane, ["A", "B", "C*"], cx);
3486        pane.update(cx, |pane, cx| {
3487            pane.add_item(
3488                Box::new(cx.new_view(|cx| TestItem::new(cx).with_label("D"))),
3489                false,
3490                false,
3491                None,
3492                cx,
3493            );
3494        });
3495        assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
3496    }
3497
3498    #[gpui::test]
3499    async fn test_add_item_with_existing_item(cx: &mut TestAppContext) {
3500        init_test(cx);
3501        let fs = FakeFs::new(cx.executor());
3502
3503        let project = Project::test(fs, None, cx).await;
3504        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
3505        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3506
3507        // 1. Add with a destination index
3508        //   1a. Add before the active item
3509        let [_, _, _, d] = set_labeled_items(&pane, ["A", "B*", "C", "D"], cx);
3510        pane.update(cx, |pane, cx| {
3511            pane.add_item(d, false, false, Some(0), cx);
3512        });
3513        assert_item_labels(&pane, ["D*", "A", "B", "C"], cx);
3514
3515        //   1b. Add after the active item
3516        let [_, _, _, d] = set_labeled_items(&pane, ["A", "B*", "C", "D"], cx);
3517        pane.update(cx, |pane, cx| {
3518            pane.add_item(d, false, false, Some(2), cx);
3519        });
3520        assert_item_labels(&pane, ["A", "B", "D*", "C"], cx);
3521
3522        //   1c. Add at the end of the item list (including off the length)
3523        let [a, _, _, _] = set_labeled_items(&pane, ["A", "B*", "C", "D"], cx);
3524        pane.update(cx, |pane, cx| {
3525            pane.add_item(a, false, false, Some(5), cx);
3526        });
3527        assert_item_labels(&pane, ["B", "C", "D", "A*"], cx);
3528
3529        //   1d. Add same item to active index
3530        let [_, b, _] = set_labeled_items(&pane, ["A", "B*", "C"], cx);
3531        pane.update(cx, |pane, cx| {
3532            pane.add_item(b, false, false, Some(1), cx);
3533        });
3534        assert_item_labels(&pane, ["A", "B*", "C"], cx);
3535
3536        //   1e. Add item to index after same item in last position
3537        let [_, _, c] = set_labeled_items(&pane, ["A", "B*", "C"], cx);
3538        pane.update(cx, |pane, cx| {
3539            pane.add_item(c, false, false, Some(2), cx);
3540        });
3541        assert_item_labels(&pane, ["A", "B", "C*"], cx);
3542
3543        // 2. Add without a destination index
3544        //   2a. Add with active item at the start of the item list
3545        let [_, _, _, d] = set_labeled_items(&pane, ["A*", "B", "C", "D"], cx);
3546        pane.update(cx, |pane, cx| {
3547            pane.add_item(d, false, false, None, cx);
3548        });
3549        assert_item_labels(&pane, ["A", "D*", "B", "C"], cx);
3550
3551        //   2b. Add with active item at the end of the item list
3552        let [a, _, _, _] = set_labeled_items(&pane, ["A", "B", "C", "D*"], cx);
3553        pane.update(cx, |pane, cx| {
3554            pane.add_item(a, false, false, None, cx);
3555        });
3556        assert_item_labels(&pane, ["B", "C", "D", "A*"], cx);
3557
3558        //   2c. Add active item to active item at end of list
3559        let [_, _, c] = set_labeled_items(&pane, ["A", "B", "C*"], cx);
3560        pane.update(cx, |pane, cx| {
3561            pane.add_item(c, false, false, None, cx);
3562        });
3563        assert_item_labels(&pane, ["A", "B", "C*"], cx);
3564
3565        //   2d. Add active item to active item at start of list
3566        let [a, _, _] = set_labeled_items(&pane, ["A*", "B", "C"], cx);
3567        pane.update(cx, |pane, cx| {
3568            pane.add_item(a, false, false, None, cx);
3569        });
3570        assert_item_labels(&pane, ["A*", "B", "C"], cx);
3571    }
3572
3573    #[gpui::test]
3574    async fn test_add_item_with_same_project_entries(cx: &mut TestAppContext) {
3575        init_test(cx);
3576        let fs = FakeFs::new(cx.executor());
3577
3578        let project = Project::test(fs, None, cx).await;
3579        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
3580        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3581
3582        // singleton view
3583        pane.update(cx, |pane, cx| {
3584            pane.add_item(
3585                Box::new(cx.new_view(|cx| {
3586                    TestItem::new(cx)
3587                        .with_singleton(true)
3588                        .with_label("buffer 1")
3589                        .with_project_items(&[TestProjectItem::new(1, "one.txt", cx)])
3590                })),
3591                false,
3592                false,
3593                None,
3594                cx,
3595            );
3596        });
3597        assert_item_labels(&pane, ["buffer 1*"], cx);
3598
3599        // new singleton view with the same project entry
3600        pane.update(cx, |pane, cx| {
3601            pane.add_item(
3602                Box::new(cx.new_view(|cx| {
3603                    TestItem::new(cx)
3604                        .with_singleton(true)
3605                        .with_label("buffer 1")
3606                        .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
3607                })),
3608                false,
3609                false,
3610                None,
3611                cx,
3612            );
3613        });
3614        assert_item_labels(&pane, ["buffer 1*"], cx);
3615
3616        // new singleton view with different project entry
3617        pane.update(cx, |pane, cx| {
3618            pane.add_item(
3619                Box::new(cx.new_view(|cx| {
3620                    TestItem::new(cx)
3621                        .with_singleton(true)
3622                        .with_label("buffer 2")
3623                        .with_project_items(&[TestProjectItem::new(2, "2.txt", cx)])
3624                })),
3625                false,
3626                false,
3627                None,
3628                cx,
3629            );
3630        });
3631        assert_item_labels(&pane, ["buffer 1", "buffer 2*"], cx);
3632
3633        // new multibuffer view with the same project entry
3634        pane.update(cx, |pane, cx| {
3635            pane.add_item(
3636                Box::new(cx.new_view(|cx| {
3637                    TestItem::new(cx)
3638                        .with_singleton(false)
3639                        .with_label("multibuffer 1")
3640                        .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
3641                })),
3642                false,
3643                false,
3644                None,
3645                cx,
3646            );
3647        });
3648        assert_item_labels(&pane, ["buffer 1", "buffer 2", "multibuffer 1*"], cx);
3649
3650        // another multibuffer view with the same project entry
3651        pane.update(cx, |pane, cx| {
3652            pane.add_item(
3653                Box::new(cx.new_view(|cx| {
3654                    TestItem::new(cx)
3655                        .with_singleton(false)
3656                        .with_label("multibuffer 1b")
3657                        .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
3658                })),
3659                false,
3660                false,
3661                None,
3662                cx,
3663            );
3664        });
3665        assert_item_labels(
3666            &pane,
3667            ["buffer 1", "buffer 2", "multibuffer 1", "multibuffer 1b*"],
3668            cx,
3669        );
3670    }
3671
3672    #[gpui::test]
3673    async fn test_remove_item_ordering_history(cx: &mut TestAppContext) {
3674        init_test(cx);
3675        let fs = FakeFs::new(cx.executor());
3676
3677        let project = Project::test(fs, None, cx).await;
3678        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
3679        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3680
3681        add_labeled_item(&pane, "A", false, cx);
3682        add_labeled_item(&pane, "B", false, cx);
3683        add_labeled_item(&pane, "C", false, cx);
3684        add_labeled_item(&pane, "D", false, cx);
3685        assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
3686
3687        pane.update(cx, |pane, cx| pane.activate_item(1, false, false, cx));
3688        add_labeled_item(&pane, "1", false, cx);
3689        assert_item_labels(&pane, ["A", "B", "1*", "C", "D"], cx);
3690
3691        pane.update(cx, |pane, cx| {
3692            pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
3693        })
3694        .unwrap()
3695        .await
3696        .unwrap();
3697        assert_item_labels(&pane, ["A", "B*", "C", "D"], cx);
3698
3699        pane.update(cx, |pane, cx| pane.activate_item(3, false, false, cx));
3700        assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
3701
3702        pane.update(cx, |pane, cx| {
3703            pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
3704        })
3705        .unwrap()
3706        .await
3707        .unwrap();
3708        assert_item_labels(&pane, ["A", "B*", "C"], cx);
3709
3710        pane.update(cx, |pane, cx| {
3711            pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
3712        })
3713        .unwrap()
3714        .await
3715        .unwrap();
3716        assert_item_labels(&pane, ["A", "C*"], cx);
3717
3718        pane.update(cx, |pane, cx| {
3719            pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
3720        })
3721        .unwrap()
3722        .await
3723        .unwrap();
3724        assert_item_labels(&pane, ["A*"], cx);
3725    }
3726
3727    #[gpui::test]
3728    async fn test_remove_item_ordering_neighbour(cx: &mut TestAppContext) {
3729        init_test(cx);
3730        cx.update_global::<SettingsStore, ()>(|s, cx| {
3731            s.update_user_settings::<ItemSettings>(cx, |s| {
3732                s.activate_on_close = Some(ActivateOnClose::Neighbour);
3733            });
3734        });
3735        let fs = FakeFs::new(cx.executor());
3736
3737        let project = Project::test(fs, None, cx).await;
3738        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
3739        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3740
3741        add_labeled_item(&pane, "A", false, cx);
3742        add_labeled_item(&pane, "B", false, cx);
3743        add_labeled_item(&pane, "C", false, cx);
3744        add_labeled_item(&pane, "D", false, cx);
3745        assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
3746
3747        pane.update(cx, |pane, cx| pane.activate_item(1, false, false, cx));
3748        add_labeled_item(&pane, "1", false, cx);
3749        assert_item_labels(&pane, ["A", "B", "1*", "C", "D"], cx);
3750
3751        pane.update(cx, |pane, cx| {
3752            pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
3753        })
3754        .unwrap()
3755        .await
3756        .unwrap();
3757        assert_item_labels(&pane, ["A", "B", "C*", "D"], cx);
3758
3759        pane.update(cx, |pane, cx| pane.activate_item(3, false, false, cx));
3760        assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
3761
3762        pane.update(cx, |pane, cx| {
3763            pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
3764        })
3765        .unwrap()
3766        .await
3767        .unwrap();
3768        assert_item_labels(&pane, ["A", "B", "C*"], cx);
3769
3770        pane.update(cx, |pane, cx| {
3771            pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
3772        })
3773        .unwrap()
3774        .await
3775        .unwrap();
3776        assert_item_labels(&pane, ["A", "B*"], cx);
3777
3778        pane.update(cx, |pane, cx| {
3779            pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
3780        })
3781        .unwrap()
3782        .await
3783        .unwrap();
3784        assert_item_labels(&pane, ["A*"], cx);
3785    }
3786
3787    #[gpui::test]
3788    async fn test_remove_item_ordering_left_neighbour(cx: &mut TestAppContext) {
3789        init_test(cx);
3790        cx.update_global::<SettingsStore, ()>(|s, cx| {
3791            s.update_user_settings::<ItemSettings>(cx, |s| {
3792                s.activate_on_close = Some(ActivateOnClose::LeftNeighbour);
3793            });
3794        });
3795        let fs = FakeFs::new(cx.executor());
3796
3797        let project = Project::test(fs, None, cx).await;
3798        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
3799        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3800
3801        add_labeled_item(&pane, "A", false, cx);
3802        add_labeled_item(&pane, "B", false, cx);
3803        add_labeled_item(&pane, "C", false, cx);
3804        add_labeled_item(&pane, "D", false, cx);
3805        assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
3806
3807        pane.update(cx, |pane, cx| pane.activate_item(1, false, false, cx));
3808        add_labeled_item(&pane, "1", false, cx);
3809        assert_item_labels(&pane, ["A", "B", "1*", "C", "D"], cx);
3810
3811        pane.update(cx, |pane, cx| {
3812            pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
3813        })
3814        .unwrap()
3815        .await
3816        .unwrap();
3817        assert_item_labels(&pane, ["A", "B*", "C", "D"], cx);
3818
3819        pane.update(cx, |pane, cx| pane.activate_item(3, false, false, cx));
3820        assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
3821
3822        pane.update(cx, |pane, cx| {
3823            pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
3824        })
3825        .unwrap()
3826        .await
3827        .unwrap();
3828        assert_item_labels(&pane, ["A", "B", "C*"], cx);
3829
3830        pane.update(cx, |pane, cx| pane.activate_item(0, false, false, cx));
3831        assert_item_labels(&pane, ["A*", "B", "C"], cx);
3832
3833        pane.update(cx, |pane, cx| {
3834            pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
3835        })
3836        .unwrap()
3837        .await
3838        .unwrap();
3839        assert_item_labels(&pane, ["B*", "C"], cx);
3840
3841        pane.update(cx, |pane, cx| {
3842            pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
3843        })
3844        .unwrap()
3845        .await
3846        .unwrap();
3847        assert_item_labels(&pane, ["C*"], cx);
3848    }
3849
3850    #[gpui::test]
3851    async fn test_close_inactive_items(cx: &mut TestAppContext) {
3852        init_test(cx);
3853        let fs = FakeFs::new(cx.executor());
3854
3855        let project = Project::test(fs, None, cx).await;
3856        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
3857        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3858
3859        set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx);
3860
3861        pane.update(cx, |pane, cx| {
3862            pane.close_inactive_items(
3863                &CloseInactiveItems {
3864                    save_intent: None,
3865                    close_pinned: false,
3866                },
3867                cx,
3868            )
3869        })
3870        .unwrap()
3871        .await
3872        .unwrap();
3873        assert_item_labels(&pane, ["C*"], cx);
3874    }
3875
3876    #[gpui::test]
3877    async fn test_close_clean_items(cx: &mut TestAppContext) {
3878        init_test(cx);
3879        let fs = FakeFs::new(cx.executor());
3880
3881        let project = Project::test(fs, None, cx).await;
3882        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
3883        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3884
3885        add_labeled_item(&pane, "A", true, cx);
3886        add_labeled_item(&pane, "B", false, cx);
3887        add_labeled_item(&pane, "C", true, cx);
3888        add_labeled_item(&pane, "D", false, cx);
3889        add_labeled_item(&pane, "E", false, cx);
3890        assert_item_labels(&pane, ["A^", "B", "C^", "D", "E*"], cx);
3891
3892        pane.update(cx, |pane, cx| {
3893            pane.close_clean_items(
3894                &CloseCleanItems {
3895                    close_pinned: false,
3896                },
3897                cx,
3898            )
3899        })
3900        .unwrap()
3901        .await
3902        .unwrap();
3903        assert_item_labels(&pane, ["A^", "C*^"], cx);
3904    }
3905
3906    #[gpui::test]
3907    async fn test_close_items_to_the_left(cx: &mut TestAppContext) {
3908        init_test(cx);
3909        let fs = FakeFs::new(cx.executor());
3910
3911        let project = Project::test(fs, None, cx).await;
3912        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
3913        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3914
3915        set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx);
3916
3917        pane.update(cx, |pane, cx| {
3918            pane.close_items_to_the_left(
3919                &CloseItemsToTheLeft {
3920                    close_pinned: false,
3921                },
3922                cx,
3923            )
3924        })
3925        .unwrap()
3926        .await
3927        .unwrap();
3928        assert_item_labels(&pane, ["C*", "D", "E"], cx);
3929    }
3930
3931    #[gpui::test]
3932    async fn test_close_items_to_the_right(cx: &mut TestAppContext) {
3933        init_test(cx);
3934        let fs = FakeFs::new(cx.executor());
3935
3936        let project = Project::test(fs, None, cx).await;
3937        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
3938        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3939
3940        set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx);
3941
3942        pane.update(cx, |pane, cx| {
3943            pane.close_items_to_the_right(
3944                &CloseItemsToTheRight {
3945                    close_pinned: false,
3946                },
3947                cx,
3948            )
3949        })
3950        .unwrap()
3951        .await
3952        .unwrap();
3953        assert_item_labels(&pane, ["A", "B", "C*"], cx);
3954    }
3955
3956    #[gpui::test]
3957    async fn test_close_all_items(cx: &mut TestAppContext) {
3958        init_test(cx);
3959        let fs = FakeFs::new(cx.executor());
3960
3961        let project = Project::test(fs, None, cx).await;
3962        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
3963        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3964
3965        let item_a = add_labeled_item(&pane, "A", false, cx);
3966        add_labeled_item(&pane, "B", false, cx);
3967        add_labeled_item(&pane, "C", false, cx);
3968        assert_item_labels(&pane, ["A", "B", "C*"], cx);
3969
3970        pane.update(cx, |pane, cx| {
3971            let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
3972            pane.pin_tab_at(ix, cx);
3973            pane.close_all_items(
3974                &CloseAllItems {
3975                    save_intent: None,
3976                    close_pinned: false,
3977                },
3978                cx,
3979            )
3980        })
3981        .unwrap()
3982        .await
3983        .unwrap();
3984        assert_item_labels(&pane, ["A*"], cx);
3985
3986        pane.update(cx, |pane, cx| {
3987            let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
3988            pane.unpin_tab_at(ix, cx);
3989            pane.close_all_items(
3990                &CloseAllItems {
3991                    save_intent: None,
3992                    close_pinned: false,
3993                },
3994                cx,
3995            )
3996        })
3997        .unwrap()
3998        .await
3999        .unwrap();
4000
4001        assert_item_labels(&pane, [], cx);
4002
4003        add_labeled_item(&pane, "A", true, cx).update(cx, |item, cx| {
4004            item.project_items
4005                .push(TestProjectItem::new(1, "A.txt", cx))
4006        });
4007        add_labeled_item(&pane, "B", true, cx).update(cx, |item, cx| {
4008            item.project_items
4009                .push(TestProjectItem::new(2, "B.txt", cx))
4010        });
4011        add_labeled_item(&pane, "C", true, cx).update(cx, |item, cx| {
4012            item.project_items
4013                .push(TestProjectItem::new(3, "C.txt", cx))
4014        });
4015        assert_item_labels(&pane, ["A^", "B^", "C*^"], cx);
4016
4017        let save = pane
4018            .update(cx, |pane, cx| {
4019                pane.close_all_items(
4020                    &CloseAllItems {
4021                        save_intent: None,
4022                        close_pinned: false,
4023                    },
4024                    cx,
4025                )
4026            })
4027            .unwrap();
4028
4029        cx.executor().run_until_parked();
4030        cx.simulate_prompt_answer(2);
4031        save.await.unwrap();
4032        assert_item_labels(&pane, [], cx);
4033
4034        add_labeled_item(&pane, "A", true, cx);
4035        add_labeled_item(&pane, "B", true, cx);
4036        add_labeled_item(&pane, "C", true, cx);
4037        assert_item_labels(&pane, ["A^", "B^", "C*^"], cx);
4038        let save = pane
4039            .update(cx, |pane, cx| {
4040                pane.close_all_items(
4041                    &CloseAllItems {
4042                        save_intent: None,
4043                        close_pinned: false,
4044                    },
4045                    cx,
4046                )
4047            })
4048            .unwrap();
4049
4050        cx.executor().run_until_parked();
4051        cx.simulate_prompt_answer(2);
4052        save.await.unwrap();
4053        assert_item_labels(&pane, [], cx);
4054    }
4055
4056    #[gpui::test]
4057    async fn test_close_all_items_including_pinned(cx: &mut TestAppContext) {
4058        init_test(cx);
4059        let fs = FakeFs::new(cx.executor());
4060
4061        let project = Project::test(fs, None, cx).await;
4062        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
4063        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
4064
4065        let item_a = add_labeled_item(&pane, "A", false, cx);
4066        add_labeled_item(&pane, "B", false, cx);
4067        add_labeled_item(&pane, "C", false, cx);
4068        assert_item_labels(&pane, ["A", "B", "C*"], cx);
4069
4070        pane.update(cx, |pane, cx| {
4071            let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4072            pane.pin_tab_at(ix, cx);
4073            pane.close_all_items(
4074                &CloseAllItems {
4075                    save_intent: None,
4076                    close_pinned: true,
4077                },
4078                cx,
4079            )
4080        })
4081        .unwrap()
4082        .await
4083        .unwrap();
4084        assert_item_labels(&pane, [], cx);
4085    }
4086
4087    fn init_test(cx: &mut TestAppContext) {
4088        cx.update(|cx| {
4089            let settings_store = SettingsStore::test(cx);
4090            cx.set_global(settings_store);
4091            theme::init(LoadThemes::JustBase, cx);
4092            crate::init_settings(cx);
4093            Project::init_settings(cx);
4094        });
4095    }
4096
4097    fn set_max_tabs(cx: &mut TestAppContext, value: Option<usize>) {
4098        cx.update_global(|store: &mut SettingsStore, cx| {
4099            store.update_user_settings::<WorkspaceSettings>(cx, |settings| {
4100                settings.max_tabs = value.map(|v| NonZero::new(v).unwrap())
4101            });
4102        });
4103    }
4104
4105    fn add_labeled_item(
4106        pane: &View<Pane>,
4107        label: &str,
4108        is_dirty: bool,
4109        cx: &mut VisualTestContext,
4110    ) -> Box<View<TestItem>> {
4111        pane.update(cx, |pane, cx| {
4112            let labeled_item = Box::new(
4113                cx.new_view(|cx| TestItem::new(cx).with_label(label).with_dirty(is_dirty)),
4114            );
4115            pane.add_item(labeled_item.clone(), false, false, None, cx);
4116            labeled_item
4117        })
4118    }
4119
4120    fn set_labeled_items<const COUNT: usize>(
4121        pane: &View<Pane>,
4122        labels: [&str; COUNT],
4123        cx: &mut VisualTestContext,
4124    ) -> [Box<View<TestItem>>; COUNT] {
4125        pane.update(cx, |pane, cx| {
4126            pane.items.clear();
4127            let mut active_item_index = 0;
4128
4129            let mut index = 0;
4130            let items = labels.map(|mut label| {
4131                if label.ends_with('*') {
4132                    label = label.trim_end_matches('*');
4133                    active_item_index = index;
4134                }
4135
4136                let labeled_item = Box::new(cx.new_view(|cx| TestItem::new(cx).with_label(label)));
4137                pane.add_item(labeled_item.clone(), false, false, None, cx);
4138                index += 1;
4139                labeled_item
4140            });
4141
4142            pane.activate_item(active_item_index, false, false, cx);
4143
4144            items
4145        })
4146    }
4147
4148    // Assert the item label, with the active item label suffixed with a '*'
4149    #[track_caller]
4150    fn assert_item_labels<const COUNT: usize>(
4151        pane: &View<Pane>,
4152        expected_states: [&str; COUNT],
4153        cx: &mut VisualTestContext,
4154    ) {
4155        let actual_states = pane.update(cx, |pane, cx| {
4156            pane.items
4157                .iter()
4158                .enumerate()
4159                .map(|(ix, item)| {
4160                    let mut state = item
4161                        .to_any()
4162                        .downcast::<TestItem>()
4163                        .unwrap()
4164                        .read(cx)
4165                        .label
4166                        .clone();
4167                    if ix == pane.active_item_index {
4168                        state.push('*');
4169                    }
4170                    if item.is_dirty(cx) {
4171                        state.push('^');
4172                    }
4173                    state
4174                })
4175                .collect::<Vec<_>>()
4176        });
4177        assert_eq!(
4178            actual_states, expected_states,
4179            "pane items do not match expectation"
4180        );
4181    }
4182}