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