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