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, AnchorCorner, AnyElement,
  18    AppContext, AsyncWindowContext, ClickEvent, ClipboardItem, 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(AnchorCorner::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(AnchorCorner::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(
2510                anchored()
2511                    .anchor(AnchorCorner::TopRight)
2512                    .child(menu.clone()),
2513            )
2514            .with_priority(1),
2515        )
2516    }
2517
2518    pub fn set_zoomed(&mut self, zoomed: bool, cx: &mut ViewContext<Self>) {
2519        self.zoomed = zoomed;
2520        cx.notify();
2521    }
2522
2523    pub fn is_zoomed(&self) -> bool {
2524        self.zoomed
2525    }
2526
2527    fn handle_drag_move<T: 'static>(
2528        &mut self,
2529        event: &DragMoveEvent<T>,
2530        cx: &mut ViewContext<Self>,
2531    ) {
2532        let can_split_predicate = self.can_split_predicate.take();
2533        let can_split = match &can_split_predicate {
2534            Some(can_split_predicate) => can_split_predicate(self, event.dragged_item(), cx),
2535            None => false,
2536        };
2537        self.can_split_predicate = can_split_predicate;
2538        if !can_split {
2539            return;
2540        }
2541
2542        let rect = event.bounds.size;
2543
2544        let size = event.bounds.size.width.min(event.bounds.size.height)
2545            * WorkspaceSettings::get_global(cx).drop_target_size;
2546
2547        let relative_cursor = Point::new(
2548            event.event.position.x - event.bounds.left(),
2549            event.event.position.y - event.bounds.top(),
2550        );
2551
2552        let direction = if relative_cursor.x < size
2553            || relative_cursor.x > rect.width - size
2554            || relative_cursor.y < size
2555            || relative_cursor.y > rect.height - size
2556        {
2557            [
2558                SplitDirection::Up,
2559                SplitDirection::Right,
2560                SplitDirection::Down,
2561                SplitDirection::Left,
2562            ]
2563            .iter()
2564            .min_by_key(|side| match side {
2565                SplitDirection::Up => relative_cursor.y,
2566                SplitDirection::Right => rect.width - relative_cursor.x,
2567                SplitDirection::Down => rect.height - relative_cursor.y,
2568                SplitDirection::Left => relative_cursor.x,
2569            })
2570            .cloned()
2571        } else {
2572            None
2573        };
2574
2575        if direction != self.drag_split_direction {
2576            self.drag_split_direction = direction;
2577        }
2578    }
2579
2580    fn handle_tab_drop(
2581        &mut self,
2582        dragged_tab: &DraggedTab,
2583        ix: usize,
2584        cx: &mut ViewContext<'_, Self>,
2585    ) {
2586        if let Some(custom_drop_handle) = self.custom_drop_handle.clone() {
2587            if let ControlFlow::Break(()) = custom_drop_handle(self, dragged_tab, cx) {
2588                return;
2589            }
2590        }
2591        let mut to_pane = cx.view().clone();
2592        let split_direction = self.drag_split_direction;
2593        let item_id = dragged_tab.item.item_id();
2594        if let Some(preview_item_id) = self.preview_item_id {
2595            if item_id == preview_item_id {
2596                self.set_preview_item_id(None, cx);
2597            }
2598        }
2599
2600        let from_pane = dragged_tab.pane.clone();
2601        self.workspace
2602            .update(cx, |_, cx| {
2603                cx.defer(move |workspace, cx| {
2604                    if let Some(split_direction) = split_direction {
2605                        to_pane = workspace.split_pane(to_pane, split_direction, cx);
2606                    }
2607                    let old_ix = from_pane.read(cx).index_for_item_id(item_id);
2608                    let old_len = to_pane.read(cx).items.len();
2609                    move_item(&from_pane, &to_pane, item_id, ix, cx);
2610                    if to_pane == from_pane {
2611                        if let Some(old_index) = old_ix {
2612                            to_pane.update(cx, |this, _| {
2613                                if old_index < this.pinned_tab_count
2614                                    && (ix == this.items.len() || ix > this.pinned_tab_count)
2615                                {
2616                                    this.pinned_tab_count -= 1;
2617                                } else if this.has_pinned_tabs()
2618                                    && old_index >= this.pinned_tab_count
2619                                    && ix < this.pinned_tab_count
2620                                {
2621                                    this.pinned_tab_count += 1;
2622                                }
2623                            });
2624                        }
2625                    } else {
2626                        to_pane.update(cx, |this, _| {
2627                            if this.items.len() > old_len // Did we not deduplicate on drag?
2628                                && this.has_pinned_tabs()
2629                                && ix < this.pinned_tab_count
2630                            {
2631                                this.pinned_tab_count += 1;
2632                            }
2633                        });
2634                        from_pane.update(cx, |this, _| {
2635                            if let Some(index) = old_ix {
2636                                if this.pinned_tab_count > index {
2637                                    this.pinned_tab_count -= 1;
2638                                }
2639                            }
2640                        })
2641                    }
2642                });
2643            })
2644            .log_err();
2645    }
2646
2647    fn handle_dragged_selection_drop(
2648        &mut self,
2649        dragged_selection: &DraggedSelection,
2650        dragged_onto: Option<usize>,
2651        cx: &mut ViewContext<'_, Self>,
2652    ) {
2653        if let Some(custom_drop_handle) = self.custom_drop_handle.clone() {
2654            if let ControlFlow::Break(()) = custom_drop_handle(self, dragged_selection, cx) {
2655                return;
2656            }
2657        }
2658        self.handle_project_entry_drop(
2659            &dragged_selection.active_selection.entry_id,
2660            dragged_onto,
2661            cx,
2662        );
2663    }
2664
2665    fn handle_project_entry_drop(
2666        &mut self,
2667        project_entry_id: &ProjectEntryId,
2668        target: Option<usize>,
2669        cx: &mut ViewContext<'_, Self>,
2670    ) {
2671        if let Some(custom_drop_handle) = self.custom_drop_handle.clone() {
2672            if let ControlFlow::Break(()) = custom_drop_handle(self, project_entry_id, cx) {
2673                return;
2674            }
2675        }
2676        let mut to_pane = cx.view().clone();
2677        let split_direction = self.drag_split_direction;
2678        let project_entry_id = *project_entry_id;
2679        self.workspace
2680            .update(cx, |_, cx| {
2681                cx.defer(move |workspace, cx| {
2682                    if let Some(path) = workspace
2683                        .project()
2684                        .read(cx)
2685                        .path_for_entry(project_entry_id, cx)
2686                    {
2687                        let load_path_task = workspace.load_path(path, cx);
2688                        cx.spawn(|workspace, mut cx| async move {
2689                            if let Some((project_entry_id, build_item)) =
2690                                load_path_task.await.notify_async_err(&mut cx)
2691                            {
2692                                let (to_pane, new_item_handle) = workspace
2693                                    .update(&mut cx, |workspace, cx| {
2694                                        if let Some(split_direction) = split_direction {
2695                                            to_pane =
2696                                                workspace.split_pane(to_pane, split_direction, cx);
2697                                        }
2698                                        let new_item_handle = to_pane.update(cx, |pane, cx| {
2699                                            pane.open_item(
2700                                                project_entry_id,
2701                                                true,
2702                                                false,
2703                                                target,
2704                                                cx,
2705                                                build_item,
2706                                            )
2707                                        });
2708                                        (to_pane, new_item_handle)
2709                                    })
2710                                    .log_err()?;
2711                                to_pane
2712                                    .update(&mut cx, |this, cx| {
2713                                        let Some(index) = this.index_for_item(&*new_item_handle)
2714                                        else {
2715                                            return;
2716                                        };
2717
2718                                        if target.map_or(false, |target| this.is_tab_pinned(target))
2719                                        {
2720                                            this.pin_tab_at(index, cx);
2721                                        }
2722                                    })
2723                                    .ok()?
2724                            }
2725                            Some(())
2726                        })
2727                        .detach();
2728                    };
2729                });
2730            })
2731            .log_err();
2732    }
2733
2734    fn handle_external_paths_drop(
2735        &mut self,
2736        paths: &ExternalPaths,
2737        cx: &mut ViewContext<'_, Self>,
2738    ) {
2739        if let Some(custom_drop_handle) = self.custom_drop_handle.clone() {
2740            if let ControlFlow::Break(()) = custom_drop_handle(self, paths, cx) {
2741                return;
2742            }
2743        }
2744        let mut to_pane = cx.view().clone();
2745        let mut split_direction = self.drag_split_direction;
2746        let paths = paths.paths().to_vec();
2747        let is_remote = self
2748            .workspace
2749            .update(cx, |workspace, cx| {
2750                if workspace.project().read(cx).is_via_collab() {
2751                    workspace.show_error(
2752                        &anyhow::anyhow!("Cannot drop files on a remote project"),
2753                        cx,
2754                    );
2755                    true
2756                } else {
2757                    false
2758                }
2759            })
2760            .unwrap_or(true);
2761        if is_remote {
2762            return;
2763        }
2764
2765        self.workspace
2766            .update(cx, |workspace, cx| {
2767                let fs = Arc::clone(workspace.project().read(cx).fs());
2768                cx.spawn(|workspace, mut cx| async move {
2769                    let mut is_file_checks = FuturesUnordered::new();
2770                    for path in &paths {
2771                        is_file_checks.push(fs.is_file(path))
2772                    }
2773                    let mut has_files_to_open = false;
2774                    while let Some(is_file) = is_file_checks.next().await {
2775                        if is_file {
2776                            has_files_to_open = true;
2777                            break;
2778                        }
2779                    }
2780                    drop(is_file_checks);
2781                    if !has_files_to_open {
2782                        split_direction = None;
2783                    }
2784
2785                    if let Ok(open_task) = workspace.update(&mut cx, |workspace, cx| {
2786                        if let Some(split_direction) = split_direction {
2787                            to_pane = workspace.split_pane(to_pane, split_direction, cx);
2788                        }
2789                        workspace.open_paths(
2790                            paths,
2791                            OpenVisible::OnlyDirectories,
2792                            Some(to_pane.downgrade()),
2793                            cx,
2794                        )
2795                    }) {
2796                        let opened_items: Vec<_> = open_task.await;
2797                        _ = workspace.update(&mut cx, |workspace, cx| {
2798                            for item in opened_items.into_iter().flatten() {
2799                                if let Err(e) = item {
2800                                    workspace.show_error(&e, cx);
2801                                }
2802                            }
2803                        });
2804                    }
2805                })
2806                .detach();
2807            })
2808            .log_err();
2809    }
2810
2811    pub fn display_nav_history_buttons(&mut self, display: Option<bool>) {
2812        self.display_nav_history_buttons = display;
2813    }
2814
2815    fn get_non_closeable_item_ids(&self, close_pinned: bool) -> Vec<EntityId> {
2816        if close_pinned {
2817            return vec![];
2818        }
2819
2820        self.items
2821            .iter()
2822            .map(|item| item.item_id())
2823            .filter(|item_id| {
2824                if let Some(ix) = self.index_for_item_id(*item_id) {
2825                    self.is_tab_pinned(ix)
2826                } else {
2827                    true
2828                }
2829            })
2830            .collect()
2831    }
2832
2833    pub fn drag_split_direction(&self) -> Option<SplitDirection> {
2834        self.drag_split_direction
2835    }
2836
2837    pub fn set_zoom_out_on_close(&mut self, zoom_out_on_close: bool) {
2838        self.zoom_out_on_close = zoom_out_on_close;
2839    }
2840}
2841
2842impl FocusableView for Pane {
2843    fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
2844        self.focus_handle.clone()
2845    }
2846}
2847
2848impl Render for Pane {
2849    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
2850        let mut key_context = KeyContext::new_with_defaults();
2851        key_context.add("Pane");
2852        if self.active_item().is_none() {
2853            key_context.add("EmptyPane");
2854        }
2855
2856        let should_display_tab_bar = self.should_display_tab_bar.clone();
2857        let display_tab_bar = should_display_tab_bar(cx);
2858        let is_local = self.project.read(cx).is_local();
2859
2860        v_flex()
2861            .key_context(key_context)
2862            .track_focus(&self.focus_handle(cx))
2863            .size_full()
2864            .flex_none()
2865            .overflow_hidden()
2866            .on_action(cx.listener(|pane, _: &AlternateFile, cx| {
2867                pane.alternate_file(cx);
2868            }))
2869            .on_action(cx.listener(|pane, _: &SplitLeft, cx| pane.split(SplitDirection::Left, cx)))
2870            .on_action(cx.listener(|pane, _: &SplitUp, cx| pane.split(SplitDirection::Up, cx)))
2871            .on_action(cx.listener(|pane, _: &SplitHorizontal, cx| {
2872                pane.split(SplitDirection::horizontal(cx), cx)
2873            }))
2874            .on_action(cx.listener(|pane, _: &SplitVertical, cx| {
2875                pane.split(SplitDirection::vertical(cx), cx)
2876            }))
2877            .on_action(
2878                cx.listener(|pane, _: &SplitRight, cx| pane.split(SplitDirection::Right, cx)),
2879            )
2880            .on_action(cx.listener(|pane, _: &SplitDown, cx| pane.split(SplitDirection::Down, cx)))
2881            .on_action(cx.listener(|pane, _: &GoBack, cx| pane.navigate_backward(cx)))
2882            .on_action(cx.listener(|pane, _: &GoForward, cx| pane.navigate_forward(cx)))
2883            .on_action(cx.listener(|pane, _: &JoinIntoNext, cx| pane.join_into_next(cx)))
2884            .on_action(cx.listener(|pane, _: &JoinAll, cx| pane.join_all(cx)))
2885            .on_action(cx.listener(Pane::toggle_zoom))
2886            .on_action(cx.listener(|pane: &mut Pane, action: &ActivateItem, cx| {
2887                pane.activate_item(action.0, true, true, cx);
2888            }))
2889            .on_action(cx.listener(|pane: &mut Pane, _: &ActivateLastItem, cx| {
2890                pane.activate_item(pane.items.len() - 1, true, true, cx);
2891            }))
2892            .on_action(cx.listener(|pane: &mut Pane, _: &ActivatePrevItem, cx| {
2893                pane.activate_prev_item(true, cx);
2894            }))
2895            .on_action(cx.listener(|pane: &mut Pane, _: &ActivateNextItem, cx| {
2896                pane.activate_next_item(true, cx);
2897            }))
2898            .on_action(cx.listener(|pane, _: &SwapItemLeft, cx| pane.swap_item_left(cx)))
2899            .on_action(cx.listener(|pane, _: &SwapItemRight, cx| pane.swap_item_right(cx)))
2900            .on_action(cx.listener(|pane, action, cx| {
2901                pane.toggle_pin_tab(action, cx);
2902            }))
2903            .when(PreviewTabsSettings::get_global(cx).enabled, |this| {
2904                this.on_action(cx.listener(|pane: &mut Pane, _: &TogglePreviewTab, cx| {
2905                    if let Some(active_item_id) = pane.active_item().map(|i| i.item_id()) {
2906                        if pane.is_active_preview_item(active_item_id) {
2907                            pane.set_preview_item_id(None, cx);
2908                        } else {
2909                            pane.set_preview_item_id(Some(active_item_id), cx);
2910                        }
2911                    }
2912                }))
2913            })
2914            .on_action(
2915                cx.listener(|pane: &mut Self, action: &CloseActiveItem, cx| {
2916                    if let Some(task) = pane.close_active_item(action, cx) {
2917                        task.detach_and_log_err(cx)
2918                    }
2919                }),
2920            )
2921            .on_action(
2922                cx.listener(|pane: &mut Self, action: &CloseInactiveItems, cx| {
2923                    if let Some(task) = pane.close_inactive_items(action, cx) {
2924                        task.detach_and_log_err(cx)
2925                    }
2926                }),
2927            )
2928            .on_action(
2929                cx.listener(|pane: &mut Self, action: &CloseCleanItems, cx| {
2930                    if let Some(task) = pane.close_clean_items(action, cx) {
2931                        task.detach_and_log_err(cx)
2932                    }
2933                }),
2934            )
2935            .on_action(
2936                cx.listener(|pane: &mut Self, action: &CloseItemsToTheLeft, cx| {
2937                    if let Some(task) = pane.close_items_to_the_left(action, cx) {
2938                        task.detach_and_log_err(cx)
2939                    }
2940                }),
2941            )
2942            .on_action(
2943                cx.listener(|pane: &mut Self, action: &CloseItemsToTheRight, cx| {
2944                    if let Some(task) = pane.close_items_to_the_right(action, cx) {
2945                        task.detach_and_log_err(cx)
2946                    }
2947                }),
2948            )
2949            .on_action(cx.listener(|pane: &mut Self, action: &CloseAllItems, cx| {
2950                if let Some(task) = pane.close_all_items(action, cx) {
2951                    task.detach_and_log_err(cx)
2952                }
2953            }))
2954            .on_action(
2955                cx.listener(|pane: &mut Self, action: &CloseActiveItem, cx| {
2956                    if let Some(task) = pane.close_active_item(action, cx) {
2957                        task.detach_and_log_err(cx)
2958                    }
2959                }),
2960            )
2961            .on_action(
2962                cx.listener(|pane: &mut Self, action: &RevealInProjectPanel, cx| {
2963                    let entry_id = action
2964                        .entry_id
2965                        .map(ProjectEntryId::from_proto)
2966                        .or_else(|| pane.active_item()?.project_entry_ids(cx).first().copied());
2967                    if let Some(entry_id) = entry_id {
2968                        pane.project.update(cx, |_, cx| {
2969                            cx.emit(project::Event::RevealInProjectPanel(entry_id))
2970                        });
2971                    }
2972                }),
2973            )
2974            .when(self.active_item().is_some() && display_tab_bar, |pane| {
2975                pane.child(self.render_tab_bar(cx))
2976            })
2977            .child({
2978                let has_worktrees = self.project.read(cx).worktrees(cx).next().is_some();
2979                // main content
2980                div()
2981                    .flex_1()
2982                    .relative()
2983                    .group("")
2984                    .overflow_hidden()
2985                    .on_drag_move::<DraggedTab>(cx.listener(Self::handle_drag_move))
2986                    .on_drag_move::<DraggedSelection>(cx.listener(Self::handle_drag_move))
2987                    .when(is_local, |div| {
2988                        div.on_drag_move::<ExternalPaths>(cx.listener(Self::handle_drag_move))
2989                    })
2990                    .map(|div| {
2991                        if let Some(item) = self.active_item() {
2992                            div.v_flex()
2993                                .size_full()
2994                                .overflow_hidden()
2995                                .child(self.toolbar.clone())
2996                                .child(item.to_any())
2997                        } else {
2998                            let placeholder = div.h_flex().size_full().justify_center();
2999                            if has_worktrees {
3000                                placeholder
3001                            } else {
3002                                placeholder.child(
3003                                    Label::new("Open a file or project to get started.")
3004                                        .color(Color::Muted),
3005                                )
3006                            }
3007                        }
3008                    })
3009                    .child(
3010                        // drag target
3011                        div()
3012                            .invisible()
3013                            .absolute()
3014                            .bg(cx.theme().colors().drop_target_background)
3015                            .group_drag_over::<DraggedTab>("", |style| style.visible())
3016                            .group_drag_over::<DraggedSelection>("", |style| style.visible())
3017                            .when(is_local, |div| {
3018                                div.group_drag_over::<ExternalPaths>("", |style| style.visible())
3019                            })
3020                            .when_some(self.can_drop_predicate.clone(), |this, p| {
3021                                this.can_drop(move |a, cx| p(a, cx))
3022                            })
3023                            .on_drop(cx.listener(move |this, dragged_tab, cx| {
3024                                this.handle_tab_drop(dragged_tab, this.active_item_index(), cx)
3025                            }))
3026                            .on_drop(cx.listener(move |this, selection: &DraggedSelection, cx| {
3027                                this.handle_dragged_selection_drop(selection, None, cx)
3028                            }))
3029                            .on_drop(cx.listener(move |this, paths, cx| {
3030                                this.handle_external_paths_drop(paths, cx)
3031                            }))
3032                            .map(|div| {
3033                                let size = DefiniteLength::Fraction(0.5);
3034                                match self.drag_split_direction {
3035                                    None => div.top_0().right_0().bottom_0().left_0(),
3036                                    Some(SplitDirection::Up) => {
3037                                        div.top_0().left_0().right_0().h(size)
3038                                    }
3039                                    Some(SplitDirection::Down) => {
3040                                        div.left_0().bottom_0().right_0().h(size)
3041                                    }
3042                                    Some(SplitDirection::Left) => {
3043                                        div.top_0().left_0().bottom_0().w(size)
3044                                    }
3045                                    Some(SplitDirection::Right) => {
3046                                        div.top_0().bottom_0().right_0().w(size)
3047                                    }
3048                                }
3049                            }),
3050                    )
3051            })
3052            .on_mouse_down(
3053                MouseButton::Navigate(NavigationDirection::Back),
3054                cx.listener(|pane, _, cx| {
3055                    if let Some(workspace) = pane.workspace.upgrade() {
3056                        let pane = cx.view().downgrade();
3057                        cx.window_context().defer(move |cx| {
3058                            workspace.update(cx, |workspace, cx| {
3059                                workspace.go_back(pane, cx).detach_and_log_err(cx)
3060                            })
3061                        })
3062                    }
3063                }),
3064            )
3065            .on_mouse_down(
3066                MouseButton::Navigate(NavigationDirection::Forward),
3067                cx.listener(|pane, _, cx| {
3068                    if let Some(workspace) = pane.workspace.upgrade() {
3069                        let pane = cx.view().downgrade();
3070                        cx.window_context().defer(move |cx| {
3071                            workspace.update(cx, |workspace, cx| {
3072                                workspace.go_forward(pane, cx).detach_and_log_err(cx)
3073                            })
3074                        })
3075                    }
3076                }),
3077            )
3078    }
3079}
3080
3081impl ItemNavHistory {
3082    pub fn push<D: 'static + Send + Any>(&mut self, data: Option<D>, cx: &mut WindowContext) {
3083        self.history
3084            .push(data, self.item.clone(), self.is_preview, cx);
3085    }
3086
3087    pub fn pop_backward(&mut self, cx: &mut WindowContext) -> Option<NavigationEntry> {
3088        self.history.pop(NavigationMode::GoingBack, cx)
3089    }
3090
3091    pub fn pop_forward(&mut self, cx: &mut WindowContext) -> Option<NavigationEntry> {
3092        self.history.pop(NavigationMode::GoingForward, cx)
3093    }
3094}
3095
3096impl NavHistory {
3097    pub fn for_each_entry(
3098        &self,
3099        cx: &AppContext,
3100        mut f: impl FnMut(&NavigationEntry, (ProjectPath, Option<PathBuf>)),
3101    ) {
3102        let borrowed_history = self.0.lock();
3103        borrowed_history
3104            .forward_stack
3105            .iter()
3106            .chain(borrowed_history.backward_stack.iter())
3107            .chain(borrowed_history.closed_stack.iter())
3108            .for_each(|entry| {
3109                if let Some(project_and_abs_path) =
3110                    borrowed_history.paths_by_item.get(&entry.item.id())
3111                {
3112                    f(entry, project_and_abs_path.clone());
3113                } else if let Some(item) = entry.item.upgrade() {
3114                    if let Some(path) = item.project_path(cx) {
3115                        f(entry, (path, None));
3116                    }
3117                }
3118            })
3119    }
3120
3121    pub fn set_mode(&mut self, mode: NavigationMode) {
3122        self.0.lock().mode = mode;
3123    }
3124
3125    pub fn mode(&self) -> NavigationMode {
3126        self.0.lock().mode
3127    }
3128
3129    pub fn disable(&mut self) {
3130        self.0.lock().mode = NavigationMode::Disabled;
3131    }
3132
3133    pub fn enable(&mut self) {
3134        self.0.lock().mode = NavigationMode::Normal;
3135    }
3136
3137    pub fn pop(&mut self, mode: NavigationMode, cx: &mut WindowContext) -> Option<NavigationEntry> {
3138        let mut state = self.0.lock();
3139        let entry = match mode {
3140            NavigationMode::Normal | NavigationMode::Disabled | NavigationMode::ClosingItem => {
3141                return None
3142            }
3143            NavigationMode::GoingBack => &mut state.backward_stack,
3144            NavigationMode::GoingForward => &mut state.forward_stack,
3145            NavigationMode::ReopeningClosedItem => &mut state.closed_stack,
3146        }
3147        .pop_back();
3148        if entry.is_some() {
3149            state.did_update(cx);
3150        }
3151        entry
3152    }
3153
3154    pub fn push<D: 'static + Send + Any>(
3155        &mut self,
3156        data: Option<D>,
3157        item: Arc<dyn WeakItemHandle>,
3158        is_preview: bool,
3159        cx: &mut WindowContext,
3160    ) {
3161        let state = &mut *self.0.lock();
3162        match state.mode {
3163            NavigationMode::Disabled => {}
3164            NavigationMode::Normal | NavigationMode::ReopeningClosedItem => {
3165                if state.backward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
3166                    state.backward_stack.pop_front();
3167                }
3168                state.backward_stack.push_back(NavigationEntry {
3169                    item,
3170                    data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
3171                    timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
3172                    is_preview,
3173                });
3174                state.forward_stack.clear();
3175            }
3176            NavigationMode::GoingBack => {
3177                if state.forward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
3178                    state.forward_stack.pop_front();
3179                }
3180                state.forward_stack.push_back(NavigationEntry {
3181                    item,
3182                    data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
3183                    timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
3184                    is_preview,
3185                });
3186            }
3187            NavigationMode::GoingForward => {
3188                if state.backward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
3189                    state.backward_stack.pop_front();
3190                }
3191                state.backward_stack.push_back(NavigationEntry {
3192                    item,
3193                    data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
3194                    timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
3195                    is_preview,
3196                });
3197            }
3198            NavigationMode::ClosingItem => {
3199                if state.closed_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
3200                    state.closed_stack.pop_front();
3201                }
3202                state.closed_stack.push_back(NavigationEntry {
3203                    item,
3204                    data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
3205                    timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
3206                    is_preview,
3207                });
3208            }
3209        }
3210        state.did_update(cx);
3211    }
3212
3213    pub fn remove_item(&mut self, item_id: EntityId) {
3214        let mut state = self.0.lock();
3215        state.paths_by_item.remove(&item_id);
3216        state
3217            .backward_stack
3218            .retain(|entry| entry.item.id() != item_id);
3219        state
3220            .forward_stack
3221            .retain(|entry| entry.item.id() != item_id);
3222        state
3223            .closed_stack
3224            .retain(|entry| entry.item.id() != item_id);
3225    }
3226
3227    pub fn path_for_item(&self, item_id: EntityId) -> Option<(ProjectPath, Option<PathBuf>)> {
3228        self.0.lock().paths_by_item.get(&item_id).cloned()
3229    }
3230}
3231
3232impl NavHistoryState {
3233    pub fn did_update(&self, cx: &mut WindowContext) {
3234        if let Some(pane) = self.pane.upgrade() {
3235            cx.defer(move |cx| {
3236                pane.update(cx, |pane, cx| pane.history_updated(cx));
3237            });
3238        }
3239    }
3240}
3241
3242fn dirty_message_for(buffer_path: Option<ProjectPath>) -> String {
3243    let path = buffer_path
3244        .as_ref()
3245        .and_then(|p| {
3246            p.path
3247                .to_str()
3248                .and_then(|s| if s.is_empty() { None } else { Some(s) })
3249        })
3250        .unwrap_or("This buffer");
3251    let path = truncate_and_remove_front(path, 80);
3252    format!("{path} contains unsaved edits. Do you want to save it?")
3253}
3254
3255pub fn tab_details(items: &[Box<dyn ItemHandle>], cx: &AppContext) -> Vec<usize> {
3256    let mut tab_details = items.iter().map(|_| 0).collect::<Vec<_>>();
3257    let mut tab_descriptions = HashMap::default();
3258    let mut done = false;
3259    while !done {
3260        done = true;
3261
3262        // Store item indices by their tab description.
3263        for (ix, (item, detail)) in items.iter().zip(&tab_details).enumerate() {
3264            if let Some(description) = item.tab_description(*detail, cx) {
3265                if *detail == 0
3266                    || Some(&description) != item.tab_description(detail - 1, cx).as_ref()
3267                {
3268                    tab_descriptions
3269                        .entry(description)
3270                        .or_insert(Vec::new())
3271                        .push(ix);
3272                }
3273            }
3274        }
3275
3276        // If two or more items have the same tab description, increase their level
3277        // of detail and try again.
3278        for (_, item_ixs) in tab_descriptions.drain() {
3279            if item_ixs.len() > 1 {
3280                done = false;
3281                for ix in item_ixs {
3282                    tab_details[ix] += 1;
3283                }
3284            }
3285        }
3286    }
3287
3288    tab_details
3289}
3290
3291pub fn render_item_indicator(item: Box<dyn ItemHandle>, cx: &WindowContext) -> Option<Indicator> {
3292    maybe!({
3293        let indicator_color = match (item.has_conflict(cx), item.is_dirty(cx)) {
3294            (true, _) => Color::Warning,
3295            (_, true) => Color::Accent,
3296            (false, false) => return None,
3297        };
3298
3299        Some(Indicator::dot().color(indicator_color))
3300    })
3301}
3302
3303impl Render for DraggedTab {
3304    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
3305        let ui_font = ThemeSettings::get_global(cx).ui_font.clone();
3306        let label = self.item.tab_content(
3307            TabContentParams {
3308                detail: Some(self.detail),
3309                selected: false,
3310                preview: false,
3311            },
3312            cx,
3313        );
3314        Tab::new("")
3315            .toggle_state(self.is_active)
3316            .child(label)
3317            .render(cx)
3318            .font(ui_font)
3319    }
3320}
3321
3322#[cfg(test)]
3323mod tests {
3324    use std::num::NonZero;
3325
3326    use super::*;
3327    use crate::item::test::{TestItem, TestProjectItem};
3328    use gpui::{TestAppContext, VisualTestContext};
3329    use project::FakeFs;
3330    use settings::SettingsStore;
3331    use theme::LoadThemes;
3332
3333    #[gpui::test]
3334    async fn test_remove_active_empty(cx: &mut TestAppContext) {
3335        init_test(cx);
3336        let fs = FakeFs::new(cx.executor());
3337
3338        let project = Project::test(fs, None, cx).await;
3339        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
3340        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3341
3342        pane.update(cx, |pane, cx| {
3343            assert!(pane
3344                .close_active_item(&CloseActiveItem { save_intent: None }, cx)
3345                .is_none())
3346        });
3347    }
3348
3349    #[gpui::test]
3350    async fn test_add_item_capped_to_max_tabs(cx: &mut TestAppContext) {
3351        init_test(cx);
3352        let fs = FakeFs::new(cx.executor());
3353
3354        let project = Project::test(fs, None, cx).await;
3355        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
3356        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3357
3358        for i in 0..7 {
3359            add_labeled_item(&pane, format!("{}", i).as_str(), false, cx);
3360        }
3361        set_max_tabs(cx, Some(5));
3362        add_labeled_item(&pane, "7", false, cx);
3363        // Remove items to respect the max tab cap.
3364        assert_item_labels(&pane, ["3", "4", "5", "6", "7*"], cx);
3365        pane.update(cx, |pane, cx| {
3366            pane.activate_item(0, false, false, cx);
3367        });
3368        add_labeled_item(&pane, "X", false, cx);
3369        // Respect activation order.
3370        assert_item_labels(&pane, ["3", "X*", "5", "6", "7"], cx);
3371
3372        for i in 0..7 {
3373            add_labeled_item(&pane, format!("D{}", i).as_str(), true, cx);
3374        }
3375        // Keeps dirty items, even over max tab cap.
3376        assert_item_labels(
3377            &pane,
3378            ["D0^", "D1^", "D2^", "D3^", "D4^", "D5^", "D6*^"],
3379            cx,
3380        );
3381
3382        set_max_tabs(cx, None);
3383        for i in 0..7 {
3384            add_labeled_item(&pane, format!("N{}", i).as_str(), false, cx);
3385        }
3386        // No cap when max tabs is None.
3387        assert_item_labels(
3388            &pane,
3389            [
3390                "D0^", "D1^", "D2^", "D3^", "D4^", "D5^", "D6^", "N0", "N1", "N2", "N3", "N4",
3391                "N5", "N6*",
3392            ],
3393            cx,
3394        );
3395    }
3396
3397    #[gpui::test]
3398    async fn test_add_item_with_new_item(cx: &mut TestAppContext) {
3399        init_test(cx);
3400        let fs = FakeFs::new(cx.executor());
3401
3402        let project = Project::test(fs, None, cx).await;
3403        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
3404        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3405
3406        // 1. Add with a destination index
3407        //   a. Add before the active item
3408        set_labeled_items(&pane, ["A", "B*", "C"], cx);
3409        pane.update(cx, |pane, cx| {
3410            pane.add_item(
3411                Box::new(cx.new_view(|cx| TestItem::new(cx).with_label("D"))),
3412                false,
3413                false,
3414                Some(0),
3415                cx,
3416            );
3417        });
3418        assert_item_labels(&pane, ["D*", "A", "B", "C"], cx);
3419
3420        //   b. Add after the active item
3421        set_labeled_items(&pane, ["A", "B*", "C"], cx);
3422        pane.update(cx, |pane, cx| {
3423            pane.add_item(
3424                Box::new(cx.new_view(|cx| TestItem::new(cx).with_label("D"))),
3425                false,
3426                false,
3427                Some(2),
3428                cx,
3429            );
3430        });
3431        assert_item_labels(&pane, ["A", "B", "D*", "C"], cx);
3432
3433        //   c. Add at the end of the item list (including off the length)
3434        set_labeled_items(&pane, ["A", "B*", "C"], cx);
3435        pane.update(cx, |pane, cx| {
3436            pane.add_item(
3437                Box::new(cx.new_view(|cx| TestItem::new(cx).with_label("D"))),
3438                false,
3439                false,
3440                Some(5),
3441                cx,
3442            );
3443        });
3444        assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
3445
3446        // 2. Add without a destination index
3447        //   a. Add with active item at the start of the item list
3448        set_labeled_items(&pane, ["A*", "B", "C"], cx);
3449        pane.update(cx, |pane, cx| {
3450            pane.add_item(
3451                Box::new(cx.new_view(|cx| TestItem::new(cx).with_label("D"))),
3452                false,
3453                false,
3454                None,
3455                cx,
3456            );
3457        });
3458        set_labeled_items(&pane, ["A", "D*", "B", "C"], cx);
3459
3460        //   b. Add with active item at the end of the item list
3461        set_labeled_items(&pane, ["A", "B", "C*"], cx);
3462        pane.update(cx, |pane, cx| {
3463            pane.add_item(
3464                Box::new(cx.new_view(|cx| TestItem::new(cx).with_label("D"))),
3465                false,
3466                false,
3467                None,
3468                cx,
3469            );
3470        });
3471        assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
3472    }
3473
3474    #[gpui::test]
3475    async fn test_add_item_with_existing_item(cx: &mut TestAppContext) {
3476        init_test(cx);
3477        let fs = FakeFs::new(cx.executor());
3478
3479        let project = Project::test(fs, None, cx).await;
3480        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
3481        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3482
3483        // 1. Add with a destination index
3484        //   1a. Add before the active item
3485        let [_, _, _, d] = set_labeled_items(&pane, ["A", "B*", "C", "D"], cx);
3486        pane.update(cx, |pane, cx| {
3487            pane.add_item(d, false, false, Some(0), cx);
3488        });
3489        assert_item_labels(&pane, ["D*", "A", "B", "C"], cx);
3490
3491        //   1b. Add after the active item
3492        let [_, _, _, d] = set_labeled_items(&pane, ["A", "B*", "C", "D"], cx);
3493        pane.update(cx, |pane, cx| {
3494            pane.add_item(d, false, false, Some(2), cx);
3495        });
3496        assert_item_labels(&pane, ["A", "B", "D*", "C"], cx);
3497
3498        //   1c. Add at the end of the item list (including off the length)
3499        let [a, _, _, _] = set_labeled_items(&pane, ["A", "B*", "C", "D"], cx);
3500        pane.update(cx, |pane, cx| {
3501            pane.add_item(a, false, false, Some(5), cx);
3502        });
3503        assert_item_labels(&pane, ["B", "C", "D", "A*"], cx);
3504
3505        //   1d. Add same item to active index
3506        let [_, b, _] = set_labeled_items(&pane, ["A", "B*", "C"], cx);
3507        pane.update(cx, |pane, cx| {
3508            pane.add_item(b, false, false, Some(1), cx);
3509        });
3510        assert_item_labels(&pane, ["A", "B*", "C"], cx);
3511
3512        //   1e. Add item to index after same item in last position
3513        let [_, _, c] = set_labeled_items(&pane, ["A", "B*", "C"], cx);
3514        pane.update(cx, |pane, cx| {
3515            pane.add_item(c, false, false, Some(2), cx);
3516        });
3517        assert_item_labels(&pane, ["A", "B", "C*"], cx);
3518
3519        // 2. Add without a destination index
3520        //   2a. Add with active item at the start of the item list
3521        let [_, _, _, d] = set_labeled_items(&pane, ["A*", "B", "C", "D"], cx);
3522        pane.update(cx, |pane, cx| {
3523            pane.add_item(d, false, false, None, cx);
3524        });
3525        assert_item_labels(&pane, ["A", "D*", "B", "C"], cx);
3526
3527        //   2b. Add with active item at the end of the item list
3528        let [a, _, _, _] = set_labeled_items(&pane, ["A", "B", "C", "D*"], cx);
3529        pane.update(cx, |pane, cx| {
3530            pane.add_item(a, false, false, None, cx);
3531        });
3532        assert_item_labels(&pane, ["B", "C", "D", "A*"], cx);
3533
3534        //   2c. Add active item to active item at end of list
3535        let [_, _, c] = set_labeled_items(&pane, ["A", "B", "C*"], cx);
3536        pane.update(cx, |pane, cx| {
3537            pane.add_item(c, false, false, None, cx);
3538        });
3539        assert_item_labels(&pane, ["A", "B", "C*"], cx);
3540
3541        //   2d. Add active item to active item at start of list
3542        let [a, _, _] = set_labeled_items(&pane, ["A*", "B", "C"], cx);
3543        pane.update(cx, |pane, cx| {
3544            pane.add_item(a, false, false, None, cx);
3545        });
3546        assert_item_labels(&pane, ["A*", "B", "C"], cx);
3547    }
3548
3549    #[gpui::test]
3550    async fn test_add_item_with_same_project_entries(cx: &mut TestAppContext) {
3551        init_test(cx);
3552        let fs = FakeFs::new(cx.executor());
3553
3554        let project = Project::test(fs, None, cx).await;
3555        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
3556        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3557
3558        // singleton view
3559        pane.update(cx, |pane, cx| {
3560            pane.add_item(
3561                Box::new(cx.new_view(|cx| {
3562                    TestItem::new(cx)
3563                        .with_singleton(true)
3564                        .with_label("buffer 1")
3565                        .with_project_items(&[TestProjectItem::new(1, "one.txt", cx)])
3566                })),
3567                false,
3568                false,
3569                None,
3570                cx,
3571            );
3572        });
3573        assert_item_labels(&pane, ["buffer 1*"], cx);
3574
3575        // new singleton view with the same project entry
3576        pane.update(cx, |pane, cx| {
3577            pane.add_item(
3578                Box::new(cx.new_view(|cx| {
3579                    TestItem::new(cx)
3580                        .with_singleton(true)
3581                        .with_label("buffer 1")
3582                        .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
3583                })),
3584                false,
3585                false,
3586                None,
3587                cx,
3588            );
3589        });
3590        assert_item_labels(&pane, ["buffer 1*"], cx);
3591
3592        // new singleton view with different project entry
3593        pane.update(cx, |pane, cx| {
3594            pane.add_item(
3595                Box::new(cx.new_view(|cx| {
3596                    TestItem::new(cx)
3597                        .with_singleton(true)
3598                        .with_label("buffer 2")
3599                        .with_project_items(&[TestProjectItem::new(2, "2.txt", cx)])
3600                })),
3601                false,
3602                false,
3603                None,
3604                cx,
3605            );
3606        });
3607        assert_item_labels(&pane, ["buffer 1", "buffer 2*"], cx);
3608
3609        // new multibuffer view with the same project entry
3610        pane.update(cx, |pane, cx| {
3611            pane.add_item(
3612                Box::new(cx.new_view(|cx| {
3613                    TestItem::new(cx)
3614                        .with_singleton(false)
3615                        .with_label("multibuffer 1")
3616                        .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
3617                })),
3618                false,
3619                false,
3620                None,
3621                cx,
3622            );
3623        });
3624        assert_item_labels(&pane, ["buffer 1", "buffer 2", "multibuffer 1*"], cx);
3625
3626        // another multibuffer view with the same project entry
3627        pane.update(cx, |pane, cx| {
3628            pane.add_item(
3629                Box::new(cx.new_view(|cx| {
3630                    TestItem::new(cx)
3631                        .with_singleton(false)
3632                        .with_label("multibuffer 1b")
3633                        .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
3634                })),
3635                false,
3636                false,
3637                None,
3638                cx,
3639            );
3640        });
3641        assert_item_labels(
3642            &pane,
3643            ["buffer 1", "buffer 2", "multibuffer 1", "multibuffer 1b*"],
3644            cx,
3645        );
3646    }
3647
3648    #[gpui::test]
3649    async fn test_remove_item_ordering_history(cx: &mut TestAppContext) {
3650        init_test(cx);
3651        let fs = FakeFs::new(cx.executor());
3652
3653        let project = Project::test(fs, None, cx).await;
3654        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
3655        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3656
3657        add_labeled_item(&pane, "A", false, cx);
3658        add_labeled_item(&pane, "B", false, cx);
3659        add_labeled_item(&pane, "C", false, cx);
3660        add_labeled_item(&pane, "D", false, cx);
3661        assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
3662
3663        pane.update(cx, |pane, cx| pane.activate_item(1, false, false, cx));
3664        add_labeled_item(&pane, "1", false, cx);
3665        assert_item_labels(&pane, ["A", "B", "1*", "C", "D"], cx);
3666
3667        pane.update(cx, |pane, cx| {
3668            pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
3669        })
3670        .unwrap()
3671        .await
3672        .unwrap();
3673        assert_item_labels(&pane, ["A", "B*", "C", "D"], cx);
3674
3675        pane.update(cx, |pane, cx| pane.activate_item(3, false, false, cx));
3676        assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
3677
3678        pane.update(cx, |pane, cx| {
3679            pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
3680        })
3681        .unwrap()
3682        .await
3683        .unwrap();
3684        assert_item_labels(&pane, ["A", "B*", "C"], cx);
3685
3686        pane.update(cx, |pane, cx| {
3687            pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
3688        })
3689        .unwrap()
3690        .await
3691        .unwrap();
3692        assert_item_labels(&pane, ["A", "C*"], cx);
3693
3694        pane.update(cx, |pane, cx| {
3695            pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
3696        })
3697        .unwrap()
3698        .await
3699        .unwrap();
3700        assert_item_labels(&pane, ["A*"], cx);
3701    }
3702
3703    #[gpui::test]
3704    async fn test_remove_item_ordering_neighbour(cx: &mut TestAppContext) {
3705        init_test(cx);
3706        cx.update_global::<SettingsStore, ()>(|s, cx| {
3707            s.update_user_settings::<ItemSettings>(cx, |s| {
3708                s.activate_on_close = Some(ActivateOnClose::Neighbour);
3709            });
3710        });
3711        let fs = FakeFs::new(cx.executor());
3712
3713        let project = Project::test(fs, None, cx).await;
3714        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
3715        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3716
3717        add_labeled_item(&pane, "A", false, cx);
3718        add_labeled_item(&pane, "B", false, cx);
3719        add_labeled_item(&pane, "C", false, cx);
3720        add_labeled_item(&pane, "D", false, cx);
3721        assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
3722
3723        pane.update(cx, |pane, cx| pane.activate_item(1, false, false, cx));
3724        add_labeled_item(&pane, "1", false, cx);
3725        assert_item_labels(&pane, ["A", "B", "1*", "C", "D"], cx);
3726
3727        pane.update(cx, |pane, cx| {
3728            pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
3729        })
3730        .unwrap()
3731        .await
3732        .unwrap();
3733        assert_item_labels(&pane, ["A", "B", "C*", "D"], cx);
3734
3735        pane.update(cx, |pane, cx| pane.activate_item(3, false, false, cx));
3736        assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
3737
3738        pane.update(cx, |pane, cx| {
3739            pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
3740        })
3741        .unwrap()
3742        .await
3743        .unwrap();
3744        assert_item_labels(&pane, ["A", "B", "C*"], cx);
3745
3746        pane.update(cx, |pane, cx| {
3747            pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
3748        })
3749        .unwrap()
3750        .await
3751        .unwrap();
3752        assert_item_labels(&pane, ["A", "B*"], cx);
3753
3754        pane.update(cx, |pane, cx| {
3755            pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
3756        })
3757        .unwrap()
3758        .await
3759        .unwrap();
3760        assert_item_labels(&pane, ["A*"], cx);
3761    }
3762
3763    #[gpui::test]
3764    async fn test_remove_item_ordering_left_neighbour(cx: &mut TestAppContext) {
3765        init_test(cx);
3766        cx.update_global::<SettingsStore, ()>(|s, cx| {
3767            s.update_user_settings::<ItemSettings>(cx, |s| {
3768                s.activate_on_close = Some(ActivateOnClose::LeftNeighbour);
3769            });
3770        });
3771        let fs = FakeFs::new(cx.executor());
3772
3773        let project = Project::test(fs, None, cx).await;
3774        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
3775        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3776
3777        add_labeled_item(&pane, "A", false, cx);
3778        add_labeled_item(&pane, "B", false, cx);
3779        add_labeled_item(&pane, "C", false, cx);
3780        add_labeled_item(&pane, "D", false, cx);
3781        assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
3782
3783        pane.update(cx, |pane, cx| pane.activate_item(1, false, false, cx));
3784        add_labeled_item(&pane, "1", false, cx);
3785        assert_item_labels(&pane, ["A", "B", "1*", "C", "D"], cx);
3786
3787        pane.update(cx, |pane, cx| {
3788            pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
3789        })
3790        .unwrap()
3791        .await
3792        .unwrap();
3793        assert_item_labels(&pane, ["A", "B*", "C", "D"], cx);
3794
3795        pane.update(cx, |pane, cx| pane.activate_item(3, false, false, cx));
3796        assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
3797
3798        pane.update(cx, |pane, cx| {
3799            pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
3800        })
3801        .unwrap()
3802        .await
3803        .unwrap();
3804        assert_item_labels(&pane, ["A", "B", "C*"], cx);
3805
3806        pane.update(cx, |pane, cx| pane.activate_item(0, false, false, cx));
3807        assert_item_labels(&pane, ["A*", "B", "C"], cx);
3808
3809        pane.update(cx, |pane, cx| {
3810            pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
3811        })
3812        .unwrap()
3813        .await
3814        .unwrap();
3815        assert_item_labels(&pane, ["B*", "C"], cx);
3816
3817        pane.update(cx, |pane, cx| {
3818            pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
3819        })
3820        .unwrap()
3821        .await
3822        .unwrap();
3823        assert_item_labels(&pane, ["C*"], cx);
3824    }
3825
3826    #[gpui::test]
3827    async fn test_close_inactive_items(cx: &mut TestAppContext) {
3828        init_test(cx);
3829        let fs = FakeFs::new(cx.executor());
3830
3831        let project = Project::test(fs, None, cx).await;
3832        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
3833        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3834
3835        set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx);
3836
3837        pane.update(cx, |pane, cx| {
3838            pane.close_inactive_items(
3839                &CloseInactiveItems {
3840                    save_intent: None,
3841                    close_pinned: false,
3842                },
3843                cx,
3844            )
3845        })
3846        .unwrap()
3847        .await
3848        .unwrap();
3849        assert_item_labels(&pane, ["C*"], cx);
3850    }
3851
3852    #[gpui::test]
3853    async fn test_close_clean_items(cx: &mut TestAppContext) {
3854        init_test(cx);
3855        let fs = FakeFs::new(cx.executor());
3856
3857        let project = Project::test(fs, None, cx).await;
3858        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
3859        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3860
3861        add_labeled_item(&pane, "A", true, cx);
3862        add_labeled_item(&pane, "B", false, cx);
3863        add_labeled_item(&pane, "C", true, cx);
3864        add_labeled_item(&pane, "D", false, cx);
3865        add_labeled_item(&pane, "E", false, cx);
3866        assert_item_labels(&pane, ["A^", "B", "C^", "D", "E*"], cx);
3867
3868        pane.update(cx, |pane, cx| {
3869            pane.close_clean_items(
3870                &CloseCleanItems {
3871                    close_pinned: false,
3872                },
3873                cx,
3874            )
3875        })
3876        .unwrap()
3877        .await
3878        .unwrap();
3879        assert_item_labels(&pane, ["A^", "C*^"], cx);
3880    }
3881
3882    #[gpui::test]
3883    async fn test_close_items_to_the_left(cx: &mut TestAppContext) {
3884        init_test(cx);
3885        let fs = FakeFs::new(cx.executor());
3886
3887        let project = Project::test(fs, None, cx).await;
3888        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
3889        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3890
3891        set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx);
3892
3893        pane.update(cx, |pane, cx| {
3894            pane.close_items_to_the_left(
3895                &CloseItemsToTheLeft {
3896                    close_pinned: false,
3897                },
3898                cx,
3899            )
3900        })
3901        .unwrap()
3902        .await
3903        .unwrap();
3904        assert_item_labels(&pane, ["C*", "D", "E"], cx);
3905    }
3906
3907    #[gpui::test]
3908    async fn test_close_items_to_the_right(cx: &mut TestAppContext) {
3909        init_test(cx);
3910        let fs = FakeFs::new(cx.executor());
3911
3912        let project = Project::test(fs, None, cx).await;
3913        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
3914        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3915
3916        set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx);
3917
3918        pane.update(cx, |pane, cx| {
3919            pane.close_items_to_the_right(
3920                &CloseItemsToTheRight {
3921                    close_pinned: false,
3922                },
3923                cx,
3924            )
3925        })
3926        .unwrap()
3927        .await
3928        .unwrap();
3929        assert_item_labels(&pane, ["A", "B", "C*"], cx);
3930    }
3931
3932    #[gpui::test]
3933    async fn test_close_all_items(cx: &mut TestAppContext) {
3934        init_test(cx);
3935        let fs = FakeFs::new(cx.executor());
3936
3937        let project = Project::test(fs, None, cx).await;
3938        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
3939        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3940
3941        let item_a = add_labeled_item(&pane, "A", false, cx);
3942        add_labeled_item(&pane, "B", false, cx);
3943        add_labeled_item(&pane, "C", false, cx);
3944        assert_item_labels(&pane, ["A", "B", "C*"], cx);
3945
3946        pane.update(cx, |pane, cx| {
3947            let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
3948            pane.pin_tab_at(ix, cx);
3949            pane.close_all_items(
3950                &CloseAllItems {
3951                    save_intent: None,
3952                    close_pinned: false,
3953                },
3954                cx,
3955            )
3956        })
3957        .unwrap()
3958        .await
3959        .unwrap();
3960        assert_item_labels(&pane, ["A*"], cx);
3961
3962        pane.update(cx, |pane, cx| {
3963            let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
3964            pane.unpin_tab_at(ix, cx);
3965            pane.close_all_items(
3966                &CloseAllItems {
3967                    save_intent: None,
3968                    close_pinned: false,
3969                },
3970                cx,
3971            )
3972        })
3973        .unwrap()
3974        .await
3975        .unwrap();
3976
3977        assert_item_labels(&pane, [], cx);
3978
3979        add_labeled_item(&pane, "A", true, cx).update(cx, |item, cx| {
3980            item.project_items
3981                .push(TestProjectItem::new(1, "A.txt", cx))
3982        });
3983        add_labeled_item(&pane, "B", true, cx).update(cx, |item, cx| {
3984            item.project_items
3985                .push(TestProjectItem::new(2, "B.txt", cx))
3986        });
3987        add_labeled_item(&pane, "C", true, cx).update(cx, |item, cx| {
3988            item.project_items
3989                .push(TestProjectItem::new(3, "C.txt", cx))
3990        });
3991        assert_item_labels(&pane, ["A^", "B^", "C*^"], cx);
3992
3993        let save = pane
3994            .update(cx, |pane, cx| {
3995                pane.close_all_items(
3996                    &CloseAllItems {
3997                        save_intent: None,
3998                        close_pinned: false,
3999                    },
4000                    cx,
4001                )
4002            })
4003            .unwrap();
4004
4005        cx.executor().run_until_parked();
4006        cx.simulate_prompt_answer(2);
4007        save.await.unwrap();
4008        assert_item_labels(&pane, [], cx);
4009
4010        add_labeled_item(&pane, "A", true, cx);
4011        add_labeled_item(&pane, "B", true, cx);
4012        add_labeled_item(&pane, "C", true, cx);
4013        assert_item_labels(&pane, ["A^", "B^", "C*^"], cx);
4014        let save = pane
4015            .update(cx, |pane, cx| {
4016                pane.close_all_items(
4017                    &CloseAllItems {
4018                        save_intent: None,
4019                        close_pinned: false,
4020                    },
4021                    cx,
4022                )
4023            })
4024            .unwrap();
4025
4026        cx.executor().run_until_parked();
4027        cx.simulate_prompt_answer(2);
4028        save.await.unwrap();
4029        assert_item_labels(&pane, [], cx);
4030    }
4031
4032    #[gpui::test]
4033    async fn test_close_all_items_including_pinned(cx: &mut TestAppContext) {
4034        init_test(cx);
4035        let fs = FakeFs::new(cx.executor());
4036
4037        let project = Project::test(fs, None, cx).await;
4038        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
4039        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
4040
4041        let item_a = add_labeled_item(&pane, "A", false, cx);
4042        add_labeled_item(&pane, "B", false, cx);
4043        add_labeled_item(&pane, "C", false, cx);
4044        assert_item_labels(&pane, ["A", "B", "C*"], cx);
4045
4046        pane.update(cx, |pane, cx| {
4047            let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4048            pane.pin_tab_at(ix, cx);
4049            pane.close_all_items(
4050                &CloseAllItems {
4051                    save_intent: None,
4052                    close_pinned: true,
4053                },
4054                cx,
4055            )
4056        })
4057        .unwrap()
4058        .await
4059        .unwrap();
4060        assert_item_labels(&pane, [], cx);
4061    }
4062
4063    fn init_test(cx: &mut TestAppContext) {
4064        cx.update(|cx| {
4065            let settings_store = SettingsStore::test(cx);
4066            cx.set_global(settings_store);
4067            theme::init(LoadThemes::JustBase, cx);
4068            crate::init_settings(cx);
4069            Project::init_settings(cx);
4070        });
4071    }
4072
4073    fn set_max_tabs(cx: &mut TestAppContext, value: Option<usize>) {
4074        cx.update_global(|store: &mut SettingsStore, cx| {
4075            store.update_user_settings::<WorkspaceSettings>(cx, |settings| {
4076                settings.max_tabs = value.map(|v| NonZero::new(v).unwrap())
4077            });
4078        });
4079    }
4080
4081    fn add_labeled_item(
4082        pane: &View<Pane>,
4083        label: &str,
4084        is_dirty: bool,
4085        cx: &mut VisualTestContext,
4086    ) -> Box<View<TestItem>> {
4087        pane.update(cx, |pane, cx| {
4088            let labeled_item = Box::new(
4089                cx.new_view(|cx| TestItem::new(cx).with_label(label).with_dirty(is_dirty)),
4090            );
4091            pane.add_item(labeled_item.clone(), false, false, None, cx);
4092            labeled_item
4093        })
4094    }
4095
4096    fn set_labeled_items<const COUNT: usize>(
4097        pane: &View<Pane>,
4098        labels: [&str; COUNT],
4099        cx: &mut VisualTestContext,
4100    ) -> [Box<View<TestItem>>; COUNT] {
4101        pane.update(cx, |pane, cx| {
4102            pane.items.clear();
4103            let mut active_item_index = 0;
4104
4105            let mut index = 0;
4106            let items = labels.map(|mut label| {
4107                if label.ends_with('*') {
4108                    label = label.trim_end_matches('*');
4109                    active_item_index = index;
4110                }
4111
4112                let labeled_item = Box::new(cx.new_view(|cx| TestItem::new(cx).with_label(label)));
4113                pane.add_item(labeled_item.clone(), false, false, None, cx);
4114                index += 1;
4115                labeled_item
4116            });
4117
4118            pane.activate_item(active_item_index, false, false, cx);
4119
4120            items
4121        })
4122    }
4123
4124    // Assert the item label, with the active item label suffixed with a '*'
4125    #[track_caller]
4126    fn assert_item_labels<const COUNT: usize>(
4127        pane: &View<Pane>,
4128        expected_states: [&str; COUNT],
4129        cx: &mut VisualTestContext,
4130    ) {
4131        let actual_states = pane.update(cx, |pane, cx| {
4132            pane.items
4133                .iter()
4134                .enumerate()
4135                .map(|(ix, item)| {
4136                    let mut state = item
4137                        .to_any()
4138                        .downcast::<TestItem>()
4139                        .unwrap()
4140                        .read(cx)
4141                        .label
4142                        .clone();
4143                    if ix == pane.active_item_index {
4144                        state.push('*');
4145                    }
4146                    if item.is_dirty(cx) {
4147                        state.push('^');
4148                    }
4149                    state
4150                })
4151                .collect::<Vec<_>>()
4152        });
4153        assert_eq!(
4154            actual_states, expected_states,
4155            "pane items do not match expectation"
4156        );
4157    }
4158}