pane.rs

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