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