pane.rs

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