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