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