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        if self
3099            .item
3100            .upgrade()
3101            .is_some_and(|item| item.include_in_nav_history())
3102        {
3103            self.history
3104                .push(data, self.item.clone(), self.is_preview, cx);
3105        }
3106    }
3107
3108    pub fn pop_backward(&mut self, cx: &mut WindowContext) -> Option<NavigationEntry> {
3109        self.history.pop(NavigationMode::GoingBack, cx)
3110    }
3111
3112    pub fn pop_forward(&mut self, cx: &mut WindowContext) -> Option<NavigationEntry> {
3113        self.history.pop(NavigationMode::GoingForward, cx)
3114    }
3115}
3116
3117impl NavHistory {
3118    pub fn for_each_entry(
3119        &self,
3120        cx: &AppContext,
3121        mut f: impl FnMut(&NavigationEntry, (ProjectPath, Option<PathBuf>)),
3122    ) {
3123        let borrowed_history = self.0.lock();
3124        borrowed_history
3125            .forward_stack
3126            .iter()
3127            .chain(borrowed_history.backward_stack.iter())
3128            .chain(borrowed_history.closed_stack.iter())
3129            .for_each(|entry| {
3130                if let Some(project_and_abs_path) =
3131                    borrowed_history.paths_by_item.get(&entry.item.id())
3132                {
3133                    f(entry, project_and_abs_path.clone());
3134                } else if let Some(item) = entry.item.upgrade() {
3135                    if let Some(path) = item.project_path(cx) {
3136                        f(entry, (path, None));
3137                    }
3138                }
3139            })
3140    }
3141
3142    pub fn set_mode(&mut self, mode: NavigationMode) {
3143        self.0.lock().mode = mode;
3144    }
3145
3146    pub fn mode(&self) -> NavigationMode {
3147        self.0.lock().mode
3148    }
3149
3150    pub fn disable(&mut self) {
3151        self.0.lock().mode = NavigationMode::Disabled;
3152    }
3153
3154    pub fn enable(&mut self) {
3155        self.0.lock().mode = NavigationMode::Normal;
3156    }
3157
3158    pub fn pop(&mut self, mode: NavigationMode, cx: &mut WindowContext) -> Option<NavigationEntry> {
3159        let mut state = self.0.lock();
3160        let entry = match mode {
3161            NavigationMode::Normal | NavigationMode::Disabled | NavigationMode::ClosingItem => {
3162                return None
3163            }
3164            NavigationMode::GoingBack => &mut state.backward_stack,
3165            NavigationMode::GoingForward => &mut state.forward_stack,
3166            NavigationMode::ReopeningClosedItem => &mut state.closed_stack,
3167        }
3168        .pop_back();
3169        if entry.is_some() {
3170            state.did_update(cx);
3171        }
3172        entry
3173    }
3174
3175    pub fn push<D: 'static + Send + Any>(
3176        &mut self,
3177        data: Option<D>,
3178        item: Arc<dyn WeakItemHandle>,
3179        is_preview: bool,
3180        cx: &mut WindowContext,
3181    ) {
3182        let state = &mut *self.0.lock();
3183        match state.mode {
3184            NavigationMode::Disabled => {}
3185            NavigationMode::Normal | NavigationMode::ReopeningClosedItem => {
3186                if state.backward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
3187                    state.backward_stack.pop_front();
3188                }
3189                state.backward_stack.push_back(NavigationEntry {
3190                    item,
3191                    data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
3192                    timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
3193                    is_preview,
3194                });
3195                state.forward_stack.clear();
3196            }
3197            NavigationMode::GoingBack => {
3198                if state.forward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
3199                    state.forward_stack.pop_front();
3200                }
3201                state.forward_stack.push_back(NavigationEntry {
3202                    item,
3203                    data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
3204                    timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
3205                    is_preview,
3206                });
3207            }
3208            NavigationMode::GoingForward => {
3209                if state.backward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
3210                    state.backward_stack.pop_front();
3211                }
3212                state.backward_stack.push_back(NavigationEntry {
3213                    item,
3214                    data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
3215                    timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
3216                    is_preview,
3217                });
3218            }
3219            NavigationMode::ClosingItem => {
3220                if state.closed_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
3221                    state.closed_stack.pop_front();
3222                }
3223                state.closed_stack.push_back(NavigationEntry {
3224                    item,
3225                    data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
3226                    timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
3227                    is_preview,
3228                });
3229            }
3230        }
3231        state.did_update(cx);
3232    }
3233
3234    pub fn remove_item(&mut self, item_id: EntityId) {
3235        let mut state = self.0.lock();
3236        state.paths_by_item.remove(&item_id);
3237        state
3238            .backward_stack
3239            .retain(|entry| entry.item.id() != item_id);
3240        state
3241            .forward_stack
3242            .retain(|entry| entry.item.id() != item_id);
3243        state
3244            .closed_stack
3245            .retain(|entry| entry.item.id() != item_id);
3246    }
3247
3248    pub fn path_for_item(&self, item_id: EntityId) -> Option<(ProjectPath, Option<PathBuf>)> {
3249        self.0.lock().paths_by_item.get(&item_id).cloned()
3250    }
3251}
3252
3253impl NavHistoryState {
3254    pub fn did_update(&self, cx: &mut WindowContext) {
3255        if let Some(pane) = self.pane.upgrade() {
3256            cx.defer(move |cx| {
3257                pane.update(cx, |pane, cx| pane.history_updated(cx));
3258            });
3259        }
3260    }
3261}
3262
3263fn dirty_message_for(buffer_path: Option<ProjectPath>) -> String {
3264    let path = buffer_path
3265        .as_ref()
3266        .and_then(|p| {
3267            p.path
3268                .to_str()
3269                .and_then(|s| if s.is_empty() { None } else { Some(s) })
3270        })
3271        .unwrap_or("This buffer");
3272    let path = truncate_and_remove_front(path, 80);
3273    format!("{path} contains unsaved edits. Do you want to save it?")
3274}
3275
3276pub fn tab_details(items: &[Box<dyn ItemHandle>], cx: &AppContext) -> Vec<usize> {
3277    let mut tab_details = items.iter().map(|_| 0).collect::<Vec<_>>();
3278    let mut tab_descriptions = HashMap::default();
3279    let mut done = false;
3280    while !done {
3281        done = true;
3282
3283        // Store item indices by their tab description.
3284        for (ix, (item, detail)) in items.iter().zip(&tab_details).enumerate() {
3285            if let Some(description) = item.tab_description(*detail, cx) {
3286                if *detail == 0
3287                    || Some(&description) != item.tab_description(detail - 1, cx).as_ref()
3288                {
3289                    tab_descriptions
3290                        .entry(description)
3291                        .or_insert(Vec::new())
3292                        .push(ix);
3293                }
3294            }
3295        }
3296
3297        // If two or more items have the same tab description, increase their level
3298        // of detail and try again.
3299        for (_, item_ixs) in tab_descriptions.drain() {
3300            if item_ixs.len() > 1 {
3301                done = false;
3302                for ix in item_ixs {
3303                    tab_details[ix] += 1;
3304                }
3305            }
3306        }
3307    }
3308
3309    tab_details
3310}
3311
3312pub fn render_item_indicator(item: Box<dyn ItemHandle>, cx: &WindowContext) -> Option<Indicator> {
3313    maybe!({
3314        let indicator_color = match (item.has_conflict(cx), item.is_dirty(cx)) {
3315            (true, _) => Color::Warning,
3316            (_, true) => Color::Accent,
3317            (false, false) => return None,
3318        };
3319
3320        Some(Indicator::dot().color(indicator_color))
3321    })
3322}
3323
3324impl Render for DraggedTab {
3325    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
3326        let ui_font = ThemeSettings::get_global(cx).ui_font.clone();
3327        let label = self.item.tab_content(
3328            TabContentParams {
3329                detail: Some(self.detail),
3330                selected: false,
3331                preview: false,
3332            },
3333            cx,
3334        );
3335        Tab::new("")
3336            .toggle_state(self.is_active)
3337            .child(label)
3338            .render(cx)
3339            .font(ui_font)
3340    }
3341}
3342
3343#[cfg(test)]
3344mod tests {
3345    use std::num::NonZero;
3346
3347    use super::*;
3348    use crate::item::test::{TestItem, TestProjectItem};
3349    use gpui::{TestAppContext, VisualTestContext};
3350    use project::FakeFs;
3351    use settings::SettingsStore;
3352    use theme::LoadThemes;
3353
3354    #[gpui::test]
3355    async fn test_remove_active_empty(cx: &mut TestAppContext) {
3356        init_test(cx);
3357        let fs = FakeFs::new(cx.executor());
3358
3359        let project = Project::test(fs, None, cx).await;
3360        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
3361        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3362
3363        pane.update(cx, |pane, cx| {
3364            assert!(pane
3365                .close_active_item(&CloseActiveItem { save_intent: None }, cx)
3366                .is_none())
3367        });
3368    }
3369
3370    #[gpui::test]
3371    async fn test_add_item_capped_to_max_tabs(cx: &mut TestAppContext) {
3372        init_test(cx);
3373        let fs = FakeFs::new(cx.executor());
3374
3375        let project = Project::test(fs, None, cx).await;
3376        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
3377        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3378
3379        for i in 0..7 {
3380            add_labeled_item(&pane, format!("{}", i).as_str(), false, cx);
3381        }
3382        set_max_tabs(cx, Some(5));
3383        add_labeled_item(&pane, "7", false, cx);
3384        // Remove items to respect the max tab cap.
3385        assert_item_labels(&pane, ["3", "4", "5", "6", "7*"], cx);
3386        pane.update(cx, |pane, cx| {
3387            pane.activate_item(0, false, false, cx);
3388        });
3389        add_labeled_item(&pane, "X", false, cx);
3390        // Respect activation order.
3391        assert_item_labels(&pane, ["3", "X*", "5", "6", "7"], cx);
3392
3393        for i in 0..7 {
3394            add_labeled_item(&pane, format!("D{}", i).as_str(), true, cx);
3395        }
3396        // Keeps dirty items, even over max tab cap.
3397        assert_item_labels(
3398            &pane,
3399            ["D0^", "D1^", "D2^", "D3^", "D4^", "D5^", "D6*^"],
3400            cx,
3401        );
3402
3403        set_max_tabs(cx, None);
3404        for i in 0..7 {
3405            add_labeled_item(&pane, format!("N{}", i).as_str(), false, cx);
3406        }
3407        // No cap when max tabs is None.
3408        assert_item_labels(
3409            &pane,
3410            [
3411                "D0^", "D1^", "D2^", "D3^", "D4^", "D5^", "D6^", "N0", "N1", "N2", "N3", "N4",
3412                "N5", "N6*",
3413            ],
3414            cx,
3415        );
3416    }
3417
3418    #[gpui::test]
3419    async fn test_add_item_with_new_item(cx: &mut TestAppContext) {
3420        init_test(cx);
3421        let fs = FakeFs::new(cx.executor());
3422
3423        let project = Project::test(fs, None, cx).await;
3424        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
3425        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3426
3427        // 1. Add with a destination index
3428        //   a. Add before the active item
3429        set_labeled_items(&pane, ["A", "B*", "C"], cx);
3430        pane.update(cx, |pane, cx| {
3431            pane.add_item(
3432                Box::new(cx.new_view(|cx| TestItem::new(cx).with_label("D"))),
3433                false,
3434                false,
3435                Some(0),
3436                cx,
3437            );
3438        });
3439        assert_item_labels(&pane, ["D*", "A", "B", "C"], cx);
3440
3441        //   b. Add after the active item
3442        set_labeled_items(&pane, ["A", "B*", "C"], cx);
3443        pane.update(cx, |pane, cx| {
3444            pane.add_item(
3445                Box::new(cx.new_view(|cx| TestItem::new(cx).with_label("D"))),
3446                false,
3447                false,
3448                Some(2),
3449                cx,
3450            );
3451        });
3452        assert_item_labels(&pane, ["A", "B", "D*", "C"], cx);
3453
3454        //   c. Add at the end of the item list (including off the length)
3455        set_labeled_items(&pane, ["A", "B*", "C"], cx);
3456        pane.update(cx, |pane, cx| {
3457            pane.add_item(
3458                Box::new(cx.new_view(|cx| TestItem::new(cx).with_label("D"))),
3459                false,
3460                false,
3461                Some(5),
3462                cx,
3463            );
3464        });
3465        assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
3466
3467        // 2. Add without a destination index
3468        //   a. Add with active item at the start of the item list
3469        set_labeled_items(&pane, ["A*", "B", "C"], cx);
3470        pane.update(cx, |pane, cx| {
3471            pane.add_item(
3472                Box::new(cx.new_view(|cx| TestItem::new(cx).with_label("D"))),
3473                false,
3474                false,
3475                None,
3476                cx,
3477            );
3478        });
3479        set_labeled_items(&pane, ["A", "D*", "B", "C"], cx);
3480
3481        //   b. Add with active item at the end of the item list
3482        set_labeled_items(&pane, ["A", "B", "C*"], cx);
3483        pane.update(cx, |pane, cx| {
3484            pane.add_item(
3485                Box::new(cx.new_view(|cx| TestItem::new(cx).with_label("D"))),
3486                false,
3487                false,
3488                None,
3489                cx,
3490            );
3491        });
3492        assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
3493    }
3494
3495    #[gpui::test]
3496    async fn test_add_item_with_existing_item(cx: &mut TestAppContext) {
3497        init_test(cx);
3498        let fs = FakeFs::new(cx.executor());
3499
3500        let project = Project::test(fs, None, cx).await;
3501        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
3502        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3503
3504        // 1. Add with a destination index
3505        //   1a. Add before the active item
3506        let [_, _, _, d] = set_labeled_items(&pane, ["A", "B*", "C", "D"], cx);
3507        pane.update(cx, |pane, cx| {
3508            pane.add_item(d, false, false, Some(0), cx);
3509        });
3510        assert_item_labels(&pane, ["D*", "A", "B", "C"], cx);
3511
3512        //   1b. Add after the active item
3513        let [_, _, _, d] = set_labeled_items(&pane, ["A", "B*", "C", "D"], cx);
3514        pane.update(cx, |pane, cx| {
3515            pane.add_item(d, false, false, Some(2), cx);
3516        });
3517        assert_item_labels(&pane, ["A", "B", "D*", "C"], cx);
3518
3519        //   1c. Add at the end of the item list (including off the length)
3520        let [a, _, _, _] = set_labeled_items(&pane, ["A", "B*", "C", "D"], cx);
3521        pane.update(cx, |pane, cx| {
3522            pane.add_item(a, false, false, Some(5), cx);
3523        });
3524        assert_item_labels(&pane, ["B", "C", "D", "A*"], cx);
3525
3526        //   1d. Add same item to active index
3527        let [_, b, _] = set_labeled_items(&pane, ["A", "B*", "C"], cx);
3528        pane.update(cx, |pane, cx| {
3529            pane.add_item(b, false, false, Some(1), cx);
3530        });
3531        assert_item_labels(&pane, ["A", "B*", "C"], cx);
3532
3533        //   1e. Add item to index after same item in last position
3534        let [_, _, c] = set_labeled_items(&pane, ["A", "B*", "C"], cx);
3535        pane.update(cx, |pane, cx| {
3536            pane.add_item(c, false, false, Some(2), cx);
3537        });
3538        assert_item_labels(&pane, ["A", "B", "C*"], cx);
3539
3540        // 2. Add without a destination index
3541        //   2a. Add with active item at the start of the item list
3542        let [_, _, _, d] = set_labeled_items(&pane, ["A*", "B", "C", "D"], cx);
3543        pane.update(cx, |pane, cx| {
3544            pane.add_item(d, false, false, None, cx);
3545        });
3546        assert_item_labels(&pane, ["A", "D*", "B", "C"], cx);
3547
3548        //   2b. Add with active item at the end of the item list
3549        let [a, _, _, _] = set_labeled_items(&pane, ["A", "B", "C", "D*"], cx);
3550        pane.update(cx, |pane, cx| {
3551            pane.add_item(a, false, false, None, cx);
3552        });
3553        assert_item_labels(&pane, ["B", "C", "D", "A*"], cx);
3554
3555        //   2c. Add active item to active item at end of list
3556        let [_, _, c] = set_labeled_items(&pane, ["A", "B", "C*"], cx);
3557        pane.update(cx, |pane, cx| {
3558            pane.add_item(c, false, false, None, cx);
3559        });
3560        assert_item_labels(&pane, ["A", "B", "C*"], cx);
3561
3562        //   2d. Add active item to active item at start of list
3563        let [a, _, _] = set_labeled_items(&pane, ["A*", "B", "C"], cx);
3564        pane.update(cx, |pane, cx| {
3565            pane.add_item(a, false, false, None, cx);
3566        });
3567        assert_item_labels(&pane, ["A*", "B", "C"], cx);
3568    }
3569
3570    #[gpui::test]
3571    async fn test_add_item_with_same_project_entries(cx: &mut TestAppContext) {
3572        init_test(cx);
3573        let fs = FakeFs::new(cx.executor());
3574
3575        let project = Project::test(fs, None, cx).await;
3576        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
3577        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3578
3579        // singleton view
3580        pane.update(cx, |pane, cx| {
3581            pane.add_item(
3582                Box::new(cx.new_view(|cx| {
3583                    TestItem::new(cx)
3584                        .with_singleton(true)
3585                        .with_label("buffer 1")
3586                        .with_project_items(&[TestProjectItem::new(1, "one.txt", cx)])
3587                })),
3588                false,
3589                false,
3590                None,
3591                cx,
3592            );
3593        });
3594        assert_item_labels(&pane, ["buffer 1*"], cx);
3595
3596        // new singleton view with the same project entry
3597        pane.update(cx, |pane, cx| {
3598            pane.add_item(
3599                Box::new(cx.new_view(|cx| {
3600                    TestItem::new(cx)
3601                        .with_singleton(true)
3602                        .with_label("buffer 1")
3603                        .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
3604                })),
3605                false,
3606                false,
3607                None,
3608                cx,
3609            );
3610        });
3611        assert_item_labels(&pane, ["buffer 1*"], cx);
3612
3613        // new singleton view with different project entry
3614        pane.update(cx, |pane, cx| {
3615            pane.add_item(
3616                Box::new(cx.new_view(|cx| {
3617                    TestItem::new(cx)
3618                        .with_singleton(true)
3619                        .with_label("buffer 2")
3620                        .with_project_items(&[TestProjectItem::new(2, "2.txt", cx)])
3621                })),
3622                false,
3623                false,
3624                None,
3625                cx,
3626            );
3627        });
3628        assert_item_labels(&pane, ["buffer 1", "buffer 2*"], cx);
3629
3630        // new multibuffer view with the same project entry
3631        pane.update(cx, |pane, cx| {
3632            pane.add_item(
3633                Box::new(cx.new_view(|cx| {
3634                    TestItem::new(cx)
3635                        .with_singleton(false)
3636                        .with_label("multibuffer 1")
3637                        .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
3638                })),
3639                false,
3640                false,
3641                None,
3642                cx,
3643            );
3644        });
3645        assert_item_labels(&pane, ["buffer 1", "buffer 2", "multibuffer 1*"], cx);
3646
3647        // another multibuffer view with the same project entry
3648        pane.update(cx, |pane, cx| {
3649            pane.add_item(
3650                Box::new(cx.new_view(|cx| {
3651                    TestItem::new(cx)
3652                        .with_singleton(false)
3653                        .with_label("multibuffer 1b")
3654                        .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
3655                })),
3656                false,
3657                false,
3658                None,
3659                cx,
3660            );
3661        });
3662        assert_item_labels(
3663            &pane,
3664            ["buffer 1", "buffer 2", "multibuffer 1", "multibuffer 1b*"],
3665            cx,
3666        );
3667    }
3668
3669    #[gpui::test]
3670    async fn test_remove_item_ordering_history(cx: &mut TestAppContext) {
3671        init_test(cx);
3672        let fs = FakeFs::new(cx.executor());
3673
3674        let project = Project::test(fs, None, cx).await;
3675        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
3676        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3677
3678        add_labeled_item(&pane, "A", false, cx);
3679        add_labeled_item(&pane, "B", false, cx);
3680        add_labeled_item(&pane, "C", false, cx);
3681        add_labeled_item(&pane, "D", false, cx);
3682        assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
3683
3684        pane.update(cx, |pane, cx| pane.activate_item(1, false, false, cx));
3685        add_labeled_item(&pane, "1", false, cx);
3686        assert_item_labels(&pane, ["A", "B", "1*", "C", "D"], cx);
3687
3688        pane.update(cx, |pane, cx| {
3689            pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
3690        })
3691        .unwrap()
3692        .await
3693        .unwrap();
3694        assert_item_labels(&pane, ["A", "B*", "C", "D"], cx);
3695
3696        pane.update(cx, |pane, cx| pane.activate_item(3, false, false, cx));
3697        assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
3698
3699        pane.update(cx, |pane, cx| {
3700            pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
3701        })
3702        .unwrap()
3703        .await
3704        .unwrap();
3705        assert_item_labels(&pane, ["A", "B*", "C"], cx);
3706
3707        pane.update(cx, |pane, cx| {
3708            pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
3709        })
3710        .unwrap()
3711        .await
3712        .unwrap();
3713        assert_item_labels(&pane, ["A", "C*"], cx);
3714
3715        pane.update(cx, |pane, cx| {
3716            pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
3717        })
3718        .unwrap()
3719        .await
3720        .unwrap();
3721        assert_item_labels(&pane, ["A*"], cx);
3722    }
3723
3724    #[gpui::test]
3725    async fn test_remove_item_ordering_neighbour(cx: &mut TestAppContext) {
3726        init_test(cx);
3727        cx.update_global::<SettingsStore, ()>(|s, cx| {
3728            s.update_user_settings::<ItemSettings>(cx, |s| {
3729                s.activate_on_close = Some(ActivateOnClose::Neighbour);
3730            });
3731        });
3732        let fs = FakeFs::new(cx.executor());
3733
3734        let project = Project::test(fs, None, cx).await;
3735        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
3736        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3737
3738        add_labeled_item(&pane, "A", false, cx);
3739        add_labeled_item(&pane, "B", false, cx);
3740        add_labeled_item(&pane, "C", false, cx);
3741        add_labeled_item(&pane, "D", false, cx);
3742        assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
3743
3744        pane.update(cx, |pane, cx| pane.activate_item(1, false, false, cx));
3745        add_labeled_item(&pane, "1", false, cx);
3746        assert_item_labels(&pane, ["A", "B", "1*", "C", "D"], cx);
3747
3748        pane.update(cx, |pane, cx| {
3749            pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
3750        })
3751        .unwrap()
3752        .await
3753        .unwrap();
3754        assert_item_labels(&pane, ["A", "B", "C*", "D"], cx);
3755
3756        pane.update(cx, |pane, cx| pane.activate_item(3, false, false, cx));
3757        assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
3758
3759        pane.update(cx, |pane, cx| {
3760            pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
3761        })
3762        .unwrap()
3763        .await
3764        .unwrap();
3765        assert_item_labels(&pane, ["A", "B", "C*"], cx);
3766
3767        pane.update(cx, |pane, cx| {
3768            pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
3769        })
3770        .unwrap()
3771        .await
3772        .unwrap();
3773        assert_item_labels(&pane, ["A", "B*"], cx);
3774
3775        pane.update(cx, |pane, cx| {
3776            pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
3777        })
3778        .unwrap()
3779        .await
3780        .unwrap();
3781        assert_item_labels(&pane, ["A*"], cx);
3782    }
3783
3784    #[gpui::test]
3785    async fn test_remove_item_ordering_left_neighbour(cx: &mut TestAppContext) {
3786        init_test(cx);
3787        cx.update_global::<SettingsStore, ()>(|s, cx| {
3788            s.update_user_settings::<ItemSettings>(cx, |s| {
3789                s.activate_on_close = Some(ActivateOnClose::LeftNeighbour);
3790            });
3791        });
3792        let fs = FakeFs::new(cx.executor());
3793
3794        let project = Project::test(fs, None, cx).await;
3795        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
3796        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3797
3798        add_labeled_item(&pane, "A", false, cx);
3799        add_labeled_item(&pane, "B", false, cx);
3800        add_labeled_item(&pane, "C", false, cx);
3801        add_labeled_item(&pane, "D", false, cx);
3802        assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
3803
3804        pane.update(cx, |pane, cx| pane.activate_item(1, false, false, cx));
3805        add_labeled_item(&pane, "1", false, cx);
3806        assert_item_labels(&pane, ["A", "B", "1*", "C", "D"], cx);
3807
3808        pane.update(cx, |pane, cx| {
3809            pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
3810        })
3811        .unwrap()
3812        .await
3813        .unwrap();
3814        assert_item_labels(&pane, ["A", "B*", "C", "D"], cx);
3815
3816        pane.update(cx, |pane, cx| pane.activate_item(3, false, false, cx));
3817        assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
3818
3819        pane.update(cx, |pane, cx| {
3820            pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
3821        })
3822        .unwrap()
3823        .await
3824        .unwrap();
3825        assert_item_labels(&pane, ["A", "B", "C*"], cx);
3826
3827        pane.update(cx, |pane, cx| pane.activate_item(0, false, false, cx));
3828        assert_item_labels(&pane, ["A*", "B", "C"], cx);
3829
3830        pane.update(cx, |pane, cx| {
3831            pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
3832        })
3833        .unwrap()
3834        .await
3835        .unwrap();
3836        assert_item_labels(&pane, ["B*", "C"], cx);
3837
3838        pane.update(cx, |pane, cx| {
3839            pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
3840        })
3841        .unwrap()
3842        .await
3843        .unwrap();
3844        assert_item_labels(&pane, ["C*"], cx);
3845    }
3846
3847    #[gpui::test]
3848    async fn test_close_inactive_items(cx: &mut TestAppContext) {
3849        init_test(cx);
3850        let fs = FakeFs::new(cx.executor());
3851
3852        let project = Project::test(fs, None, cx).await;
3853        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
3854        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3855
3856        set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx);
3857
3858        pane.update(cx, |pane, cx| {
3859            pane.close_inactive_items(
3860                &CloseInactiveItems {
3861                    save_intent: None,
3862                    close_pinned: false,
3863                },
3864                cx,
3865            )
3866        })
3867        .unwrap()
3868        .await
3869        .unwrap();
3870        assert_item_labels(&pane, ["C*"], cx);
3871    }
3872
3873    #[gpui::test]
3874    async fn test_close_clean_items(cx: &mut TestAppContext) {
3875        init_test(cx);
3876        let fs = FakeFs::new(cx.executor());
3877
3878        let project = Project::test(fs, None, cx).await;
3879        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
3880        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3881
3882        add_labeled_item(&pane, "A", true, cx);
3883        add_labeled_item(&pane, "B", false, cx);
3884        add_labeled_item(&pane, "C", true, cx);
3885        add_labeled_item(&pane, "D", false, cx);
3886        add_labeled_item(&pane, "E", false, cx);
3887        assert_item_labels(&pane, ["A^", "B", "C^", "D", "E*"], cx);
3888
3889        pane.update(cx, |pane, cx| {
3890            pane.close_clean_items(
3891                &CloseCleanItems {
3892                    close_pinned: false,
3893                },
3894                cx,
3895            )
3896        })
3897        .unwrap()
3898        .await
3899        .unwrap();
3900        assert_item_labels(&pane, ["A^", "C*^"], cx);
3901    }
3902
3903    #[gpui::test]
3904    async fn test_close_items_to_the_left(cx: &mut TestAppContext) {
3905        init_test(cx);
3906        let fs = FakeFs::new(cx.executor());
3907
3908        let project = Project::test(fs, None, cx).await;
3909        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
3910        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3911
3912        set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx);
3913
3914        pane.update(cx, |pane, cx| {
3915            pane.close_items_to_the_left(
3916                &CloseItemsToTheLeft {
3917                    close_pinned: false,
3918                },
3919                cx,
3920            )
3921        })
3922        .unwrap()
3923        .await
3924        .unwrap();
3925        assert_item_labels(&pane, ["C*", "D", "E"], cx);
3926    }
3927
3928    #[gpui::test]
3929    async fn test_close_items_to_the_right(cx: &mut TestAppContext) {
3930        init_test(cx);
3931        let fs = FakeFs::new(cx.executor());
3932
3933        let project = Project::test(fs, None, cx).await;
3934        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
3935        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3936
3937        set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx);
3938
3939        pane.update(cx, |pane, cx| {
3940            pane.close_items_to_the_right(
3941                &CloseItemsToTheRight {
3942                    close_pinned: false,
3943                },
3944                cx,
3945            )
3946        })
3947        .unwrap()
3948        .await
3949        .unwrap();
3950        assert_item_labels(&pane, ["A", "B", "C*"], cx);
3951    }
3952
3953    #[gpui::test]
3954    async fn test_close_all_items(cx: &mut TestAppContext) {
3955        init_test(cx);
3956        let fs = FakeFs::new(cx.executor());
3957
3958        let project = Project::test(fs, None, cx).await;
3959        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
3960        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3961
3962        let item_a = add_labeled_item(&pane, "A", false, cx);
3963        add_labeled_item(&pane, "B", false, cx);
3964        add_labeled_item(&pane, "C", false, cx);
3965        assert_item_labels(&pane, ["A", "B", "C*"], cx);
3966
3967        pane.update(cx, |pane, cx| {
3968            let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
3969            pane.pin_tab_at(ix, cx);
3970            pane.close_all_items(
3971                &CloseAllItems {
3972                    save_intent: None,
3973                    close_pinned: false,
3974                },
3975                cx,
3976            )
3977        })
3978        .unwrap()
3979        .await
3980        .unwrap();
3981        assert_item_labels(&pane, ["A*"], cx);
3982
3983        pane.update(cx, |pane, cx| {
3984            let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
3985            pane.unpin_tab_at(ix, cx);
3986            pane.close_all_items(
3987                &CloseAllItems {
3988                    save_intent: None,
3989                    close_pinned: false,
3990                },
3991                cx,
3992            )
3993        })
3994        .unwrap()
3995        .await
3996        .unwrap();
3997
3998        assert_item_labels(&pane, [], cx);
3999
4000        add_labeled_item(&pane, "A", true, cx).update(cx, |item, cx| {
4001            item.project_items
4002                .push(TestProjectItem::new(1, "A.txt", cx))
4003        });
4004        add_labeled_item(&pane, "B", true, cx).update(cx, |item, cx| {
4005            item.project_items
4006                .push(TestProjectItem::new(2, "B.txt", cx))
4007        });
4008        add_labeled_item(&pane, "C", true, cx).update(cx, |item, cx| {
4009            item.project_items
4010                .push(TestProjectItem::new(3, "C.txt", cx))
4011        });
4012        assert_item_labels(&pane, ["A^", "B^", "C*^"], cx);
4013
4014        let save = pane
4015            .update(cx, |pane, cx| {
4016                pane.close_all_items(
4017                    &CloseAllItems {
4018                        save_intent: None,
4019                        close_pinned: false,
4020                    },
4021                    cx,
4022                )
4023            })
4024            .unwrap();
4025
4026        cx.executor().run_until_parked();
4027        cx.simulate_prompt_answer(2);
4028        save.await.unwrap();
4029        assert_item_labels(&pane, [], cx);
4030
4031        add_labeled_item(&pane, "A", true, cx);
4032        add_labeled_item(&pane, "B", true, cx);
4033        add_labeled_item(&pane, "C", true, cx);
4034        assert_item_labels(&pane, ["A^", "B^", "C*^"], cx);
4035        let save = pane
4036            .update(cx, |pane, cx| {
4037                pane.close_all_items(
4038                    &CloseAllItems {
4039                        save_intent: None,
4040                        close_pinned: false,
4041                    },
4042                    cx,
4043                )
4044            })
4045            .unwrap();
4046
4047        cx.executor().run_until_parked();
4048        cx.simulate_prompt_answer(2);
4049        save.await.unwrap();
4050        assert_item_labels(&pane, [], cx);
4051    }
4052
4053    #[gpui::test]
4054    async fn test_close_all_items_including_pinned(cx: &mut TestAppContext) {
4055        init_test(cx);
4056        let fs = FakeFs::new(cx.executor());
4057
4058        let project = Project::test(fs, None, cx).await;
4059        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
4060        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
4061
4062        let item_a = add_labeled_item(&pane, "A", false, cx);
4063        add_labeled_item(&pane, "B", false, cx);
4064        add_labeled_item(&pane, "C", false, cx);
4065        assert_item_labels(&pane, ["A", "B", "C*"], cx);
4066
4067        pane.update(cx, |pane, cx| {
4068            let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4069            pane.pin_tab_at(ix, cx);
4070            pane.close_all_items(
4071                &CloseAllItems {
4072                    save_intent: None,
4073                    close_pinned: true,
4074                },
4075                cx,
4076            )
4077        })
4078        .unwrap()
4079        .await
4080        .unwrap();
4081        assert_item_labels(&pane, [], cx);
4082    }
4083
4084    fn init_test(cx: &mut TestAppContext) {
4085        cx.update(|cx| {
4086            let settings_store = SettingsStore::test(cx);
4087            cx.set_global(settings_store);
4088            theme::init(LoadThemes::JustBase, cx);
4089            crate::init_settings(cx);
4090            Project::init_settings(cx);
4091        });
4092    }
4093
4094    fn set_max_tabs(cx: &mut TestAppContext, value: Option<usize>) {
4095        cx.update_global(|store: &mut SettingsStore, cx| {
4096            store.update_user_settings::<WorkspaceSettings>(cx, |settings| {
4097                settings.max_tabs = value.map(|v| NonZero::new(v).unwrap())
4098            });
4099        });
4100    }
4101
4102    fn add_labeled_item(
4103        pane: &View<Pane>,
4104        label: &str,
4105        is_dirty: bool,
4106        cx: &mut VisualTestContext,
4107    ) -> Box<View<TestItem>> {
4108        pane.update(cx, |pane, cx| {
4109            let labeled_item = Box::new(
4110                cx.new_view(|cx| TestItem::new(cx).with_label(label).with_dirty(is_dirty)),
4111            );
4112            pane.add_item(labeled_item.clone(), false, false, None, cx);
4113            labeled_item
4114        })
4115    }
4116
4117    fn set_labeled_items<const COUNT: usize>(
4118        pane: &View<Pane>,
4119        labels: [&str; COUNT],
4120        cx: &mut VisualTestContext,
4121    ) -> [Box<View<TestItem>>; COUNT] {
4122        pane.update(cx, |pane, cx| {
4123            pane.items.clear();
4124            let mut active_item_index = 0;
4125
4126            let mut index = 0;
4127            let items = labels.map(|mut label| {
4128                if label.ends_with('*') {
4129                    label = label.trim_end_matches('*');
4130                    active_item_index = index;
4131                }
4132
4133                let labeled_item = Box::new(cx.new_view(|cx| TestItem::new(cx).with_label(label)));
4134                pane.add_item(labeled_item.clone(), false, false, None, cx);
4135                index += 1;
4136                labeled_item
4137            });
4138
4139            pane.activate_item(active_item_index, false, false, cx);
4140
4141            items
4142        })
4143    }
4144
4145    // Assert the item label, with the active item label suffixed with a '*'
4146    #[track_caller]
4147    fn assert_item_labels<const COUNT: usize>(
4148        pane: &View<Pane>,
4149        expected_states: [&str; COUNT],
4150        cx: &mut VisualTestContext,
4151    ) {
4152        let actual_states = pane.update(cx, |pane, cx| {
4153            pane.items
4154                .iter()
4155                .enumerate()
4156                .map(|(ix, item)| {
4157                    let mut state = item
4158                        .to_any()
4159                        .downcast::<TestItem>()
4160                        .unwrap()
4161                        .read(cx)
4162                        .label
4163                        .clone();
4164                    if ix == pane.active_item_index {
4165                        state.push('*');
4166                    }
4167                    if item.is_dirty(cx) {
4168                        state.push('^');
4169                    }
4170                    state
4171                })
4172                .collect::<Vec<_>>()
4173        });
4174        assert_eq!(
4175            actual_states, expected_states,
4176            "pane items do not match expectation"
4177        );
4178    }
4179}