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